aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.clang-format35
-rw-r--r--.github/workflows/builds.yml12
-rw-r--r--.gitignore4
-rw-r--r--README.md8
-rw-r--r--SConstruct6
-rw-r--r--docs/general-program-architecture.md4
-rw-r--r--docs/simulation/goods.md2
-rw-r--r--docs/simulation/ideologies.md4
-rw-r--r--docs/simulation/provinces.md16
-rw-r--r--docs/styleguide-cpp.md6
-rw-r--r--extension/src/Checksum.hpp8
-rw-r--r--extension/src/GameSingleton.cpp463
-rw-r--r--extension/src/GameSingleton.hpp63
-rw-r--r--extension/src/LoadLocalisation.cpp8
-rw-r--r--extension/src/LoadLocalisation.hpp9
-rw-r--r--extension/src/MapMesh.cpp150
-rw-r--r--extension/src/MapMesh.hpp34
-rw-r--r--extension/src/Simulation.hpp52
-rw-r--r--extension/src/TestSingleton.cpp36
-rw-r--r--extension/src/TestSingleton.hpp24
-rw-r--r--extension/src/openvic2/Date.cpp161
-rw-r--r--extension/src/openvic2/Date.hpp83
-rw-r--r--extension/src/openvic2/GameAdvancementHook.cpp72
-rw-r--r--extension/src/openvic2/GameAdvancementHook.hpp42
-rw-r--r--extension/src/openvic2/GameManager.cpp46
-rw-r--r--extension/src/openvic2/GameManager.hpp29
-rw-r--r--extension/src/openvic2/Logger.cpp26
-rw-r--r--extension/src/openvic2/Logger.hpp83
-rw-r--r--extension/src/openvic2/Types.cpp13
-rw-r--r--extension/src/openvic2/Types.hpp100
-rw-r--r--extension/src/openvic2/map/Building.cpp124
-rw-r--r--extension/src/openvic2/map/Building.hpp75
-rw-r--r--extension/src/openvic2/map/Map.cpp320
-rw-r--r--extension/src/openvic2/map/Map.hpp76
-rw-r--r--extension/src/openvic2/map/Province.cpp78
-rw-r--r--extension/src/openvic2/map/Province.hpp50
-rw-r--r--extension/src/openvic2/map/Region.cpp26
-rw-r--r--extension/src/openvic2/map/Region.hpp30
-rw-r--r--extension/src/register_types.cpp47
-rw-r--r--extension/src/register_types.h2
-rw-r--r--game/art/economy/goods/Aeroplanes.png.import34
-rw-r--r--game/art/economy/goods/Ammunition.png.import34
-rw-r--r--game/art/economy/goods/Artillery.png.import34
-rw-r--r--game/art/economy/goods/Automobiles.png.import34
-rw-r--r--game/art/economy/goods/CannedFood.png.import34
-rw-r--r--game/art/economy/goods/Cattle.png.import34
-rw-r--r--game/art/economy/goods/Cement.png.import34
-rw-r--r--game/art/economy/goods/ClipperConvoys.png.import34
-rw-r--r--game/art/economy/goods/Coal.png.import34
-rw-r--r--game/art/economy/goods/Coffee.png.import34
-rw-r--r--game/art/economy/goods/Cotton.png.import34
-rw-r--r--game/art/economy/goods/Dye.png.import34
-rw-r--r--game/art/economy/goods/ElectricGear.png.import34
-rw-r--r--game/art/economy/goods/Explosives.png.import34
-rw-r--r--game/art/economy/goods/Fabric.png.import34
-rw-r--r--game/art/economy/goods/Fertilizer.png.import34
-rw-r--r--game/art/economy/goods/Fish.png.import34
-rw-r--r--game/art/economy/goods/Fruit.png.import34
-rw-r--r--game/art/economy/goods/Fuel.png.import34
-rw-r--r--game/art/economy/goods/Furniture.png.import34
-rw-r--r--game/art/economy/goods/Glass.png.import34
-rw-r--r--game/art/economy/goods/Grain.png.import34
-rw-r--r--game/art/economy/goods/Iron.png.import34
-rw-r--r--game/art/economy/goods/Liquor.png.import34
-rw-r--r--game/art/economy/goods/Lumber.png.import34
-rw-r--r--game/art/economy/goods/LuxuryClothes.png.import34
-rw-r--r--game/art/economy/goods/LuxuryFurniture.png.import34
-rw-r--r--game/art/economy/goods/MachineParts.png.import34
-rw-r--r--game/art/economy/goods/Oil.png.import34
-rw-r--r--game/art/economy/goods/Opium.png.import34
-rw-r--r--game/art/economy/goods/Paper.png.import34
-rw-r--r--game/art/economy/goods/PreciousMetal.png.import34
-rw-r--r--game/art/economy/goods/Radios.png.import34
-rw-r--r--game/art/economy/goods/RegularClothes.png.import34
-rw-r--r--game/art/economy/goods/Rubber.png.import34
-rw-r--r--game/art/economy/goods/Silk.png.import34
-rw-r--r--game/art/economy/goods/SmallArms.png.import34
-rw-r--r--game/art/economy/goods/SteamerConvoys.png.import34
-rw-r--r--game/art/economy/goods/Steel.png.import34
-rw-r--r--game/art/economy/goods/Sulphur.png.import34
-rw-r--r--game/art/economy/goods/Tanks.png.import34
-rw-r--r--game/art/economy/goods/Tea.png.import34
-rw-r--r--game/art/economy/goods/Telephones.png.import34
-rw-r--r--game/art/economy/goods/Timber.png.import34
-rw-r--r--game/art/economy/goods/Tobacco.png.import34
-rw-r--r--game/art/economy/goods/TropicalWood.png.import34
-rw-r--r--game/art/economy/goods/Wine.png.import34
-rw-r--r--game/art/economy/goods/Wool.png.import34
-rw-r--r--game/art/technology/army/Army Decision Making.png.import34
-rw-r--r--game/art/technology/army/Army NCO Training.png.import34
-rw-r--r--game/art/technology/army/Army Professionalism.png.import34
-rw-r--r--game/art/technology/army/Army Risk Management.png.import34
-rw-r--r--game/art/technology/army/Bolt-action Rifle.png.import34
-rw-r--r--game/art/technology/army/Breech-Loaded Rifles.png.import34
-rw-r--r--game/art/technology/army/Bronze Muzzle-loaded Artillery.png.import34
-rw-r--r--game/art/technology/army/Deep Defense System.png.import34
-rw-r--r--game/art/technology/army/Flintlock Rifles.png.import34
-rw-r--r--game/art/technology/army/Indirect Artillery Fire.png.import34
-rw-r--r--game/art/technology/army/Infiltration.png.import34
-rw-r--r--game/art/technology/army/Iron Breech-Loaded Artillery.png.import34
-rw-r--r--game/art/technology/army/Iron Muzzle-Loaded Artillery.png.import34
-rw-r--r--game/art/technology/army/Machine Gun.png.import34
-rw-r--r--game/art/technology/army/Military Directionism.png.import34
-rw-r--r--game/art/technology/army/Military Logistics.png.import34
-rw-r--r--game/art/technology/army/Military Plans.png.import34
-rw-r--r--game/art/technology/army/Military Staff System.png.import34
-rw-r--r--game/art/technology/army/Military Statistics.png.import34
-rw-r--r--game/art/technology/army/Muzzle-loaded Rifles.png.import34
-rw-r--r--game/art/technology/army/Point Defense System.png.import34
-rw-r--r--game/art/technology/army/Post-Napoleonic Thought.png.import34
-rw-r--r--game/art/technology/army/Steel Breech-Loaded Artillery.png.import34
-rw-r--r--game/art/technology/army/Strategic Mobility.png.import34
-rw-r--r--game/art/technology/army/The Command Principle.png.import34
-rw-r--r--game/art/terrain/farmlands.pngbin0 -> 265377 bytes
-rw-r--r--game/art/terrain/farmlands.png.import34
-rw-r--r--game/art/ui/minimap.pngbin0 -> 122017 bytes
-rw-r--r--game/art/ui/minimap.png.import34
-rw-r--r--game/art/ui/minimap_frame.pngbin0 -> 1043 bytes
-rw-r--r--game/art/ui/minimap_frame.png.import34
-rw-r--r--game/art/units/Airplane.png.import34
-rw-r--r--game/art/units/Armor.png.import34
-rw-r--r--game/art/units/Artillery.png.import34
-rw-r--r--game/art/units/Battleship.png.import34
-rw-r--r--game/art/units/Cavalry.png.import34
-rw-r--r--game/art/units/ClipperTransport.png.import34
-rw-r--r--game/art/units/CommerceRaider.png.import34
-rw-r--r--game/art/units/Cruiser.png.import34
-rw-r--r--game/art/units/Cuirassier.png.import34
-rw-r--r--game/art/units/Dragoon.png.import34
-rw-r--r--game/art/units/Dreadnought.png.import34
-rw-r--r--game/art/units/Engineer.png.import34
-rw-r--r--game/art/units/Frigate.png.import34
-rw-r--r--game/art/units/Guard.png.import34
-rw-r--r--game/art/units/Hussar.png.import34
-rw-r--r--game/art/units/Infantry.png.import34
-rw-r--r--game/art/units/Ironclad.png.import34
-rw-r--r--game/art/units/Irregular.png.import34
-rw-r--r--game/art/units/ManOWar.png.import34
-rw-r--r--game/art/units/Monitor.png.import34
-rw-r--r--game/art/units/SteamerTransport.png.import34
-rw-r--r--game/common/map/provinces.json238
-rw-r--r--game/common/map/provinces.pngbin0 -> 1105620 bytes
-rw-r--r--game/common/map/provinces.png.import3
-rw-r--r--game/common/map/regions.json13
-rw-r--r--game/common/map/water.json6
-rw-r--r--game/localisation/en_GB/mapmodes.csv6
-rw-r--r--game/localisation/en_GB/mapmodes.csv.import3
-rw-r--r--game/localisation/en_GB/menus.csv27
-rw-r--r--game/localisation/en_GB/provinces.csv245
-rw-r--r--game/localisation/en_GB/provinces.csv.import3
-rw-r--r--game/localisation/en_GB/regions.csv8
-rw-r--r--game/localisation/en_GB/regions.csv.import3
-rw-r--r--game/localisation/en_US/menus.csv7
-rw-r--r--game/localisation/fr_FR/menus.csv7
-rw-r--r--game/project.godot49
-rw-r--r--game/src/Autoload/Events.gd17
-rw-r--r--game/src/GameMenu.tscn1
-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
-rw-r--r--game/src/LobbyMenu/LobbyMenu.gd5
-rw-r--r--game/src/LobbyMenu/LobbyMenu.tscn1
-rw-r--r--game/src/MainMenu/MainMenu.gd2
-rw-r--r--game/src/MainMenu/MainMenu.tscn62
-rw-r--r--game/src/MusicConductor/MusicConductor.gd2
-rw-r--r--game/src/MusicConductor/MusicPlayer.gd2
-rw-r--r--game/src/MusicConductor/MusicPlayer.tscn54
-rw-r--r--game/src/Utility/StyleBoxWithSound.gd1
-rw-r--r--game/theme/assets/OpenVicFINALREALTRANS.pngbin0 -> 57587 bytes
-rw-r--r--game/theme/assets/OpenVicFINALREALTRANS.png.import34
-rw-r--r--game/theme/game_session_menu.tres84
-rw-r--r--game/theme/main_menu.tres8
m---------godot-cpp0
184 files changed, 7377 insertions, 292 deletions
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..8f3b82a
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,35 @@
+---
+UseCRLF: false
+Standard: c++20
+UseTab: Always
+TabWidth: 4
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpacesInContainerLiterals: false
+SpacesInConditionalStatement: false
+SpacesInCStyleCastParentheses: false
+SpacesInAngles: false
+SpaceInEmptyParentheses: false
+SpaceInEmptyBlock: false
+SpaceBeforeSquareBrackets: false
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeParens: ControlStatements
+SpaceBeforeInheritanceColon: true
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeCpp11BracedList: true
+SpaceBeforeAssignmentOperators: true
+SpaceAfterTemplateKeyword: false
+SpaceAfterLogicalNot: false
+PointerAlignment: Left
+NamespaceIndentation: All
+IndentWidth: 4
+Language: Cpp
+IndentCaseLabels: true
+FixNamespaceComments: false
+Cpp11BracedListStyle: false
+ColumnLimit: 0
+CompactNamespaces: false
+BreakBeforeBraces: Attach
+AlwaysBreakTemplateDeclarations: Yes
+AlignTrailingComments: true
+AlignEscapedNewlines: Left
diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml
index 88d5200..664f8e4 100644
--- a/.github/workflows/builds.yml
+++ b/.github/workflows/builds.yml
@@ -80,6 +80,10 @@ jobs:
run: |
sudo apt-get update -qq
sudo apt-get install -qqq build-essential pkg-config
+ g++ --version
+ sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 12
+ sudo update-alternatives --set g++ /usr/bin/g++-12
+ g++ --version
- name: Setup MinGW for Windows/MinGW build
if: ${{ matrix.platform == 'windows' }}
@@ -143,8 +147,8 @@ jobs:
id: export_game
uses: Spartan322/godot-export@master
with:
- godot_executable_download_url: https://downloads.tuxfamily.org/godotengine/4.0.1/Godot_v4.0.1-stable_linux.x86_64.zip
- godot_export_templates_download_url: https://downloads.tuxfamily.org/godotengine/4.0.1/Godot_v4.0.1-stable_export_templates.tpz
+ godot_executable_download_url: https://downloads.tuxfamily.org/godotengine/4.0.2/Godot_v4.0.2-stable_linux.x86_64.zip
+ godot_export_templates_download_url: https://downloads.tuxfamily.org/godotengine/4.0.2/Godot_v4.0.2-stable_export_templates.tpz
relative_project_path: ./game
export_as_pack: true
export_debug: true
@@ -191,8 +195,8 @@ jobs:
id: export_game
uses: Spartan322/godot-export@master
with:
- godot_executable_download_url: https://downloads.tuxfamily.org/godotengine/4.0.1/Godot_v4.0.1-stable_linux.x86_64.zip
- godot_export_templates_download_url: https://downloads.tuxfamily.org/godotengine/4.0.1/Godot_v4.0.1-stable_export_templates.tpz
+ godot_executable_download_url: https://downloads.tuxfamily.org/godotengine/4.0.2/Godot_v4.0.2-stable_linux.x86_64.zip
+ godot_export_templates_download_url: https://downloads.tuxfamily.org/godotengine/4.0.2/Godot_v4.0.2-stable_export_templates.tpz
relative_project_path: ./game
archive_output: true
wine_path: ${{ steps.wine_install.outputs.WINE_PATH }}
diff --git a/.gitignore b/.gitignore
index 92118b7..596bc88 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,9 @@
!.vscode/launch.json
!.vscode/tasks.json
+# Visual Studio
+.vs/
+
# Godot 4+ specific ignores
.godot/
game/bin/openvic2/*
@@ -70,3 +73,4 @@ bin/*
.DS_Store
*.translation
+!game/common/map/*.obj \ No newline at end of file
diff --git a/README.md b/README.md
index 7f8a1d8..fed2e32 100644
--- a/README.md
+++ b/README.md
@@ -5,22 +5,22 @@ Main Repo for the OpenVic2 Project
For detailed instructions, view the Contributor Quickstart Guide [here](docs/contribution-quickstart-guide.md)
## Required
-* [Godot 4.0.1](https://github.com/godotengine/godot/releases/tag/4.0.1-stable)
+* [Godot 4.0.2](https://github.com/godotengine/godot/releases/tag/4.0.2-stable)
* [scons](https://scons.org/)
## [Godot Documentation](https://docs.godotengine.org/en/latest/)
## Build/Run Instructions
-1. Install [Godot 4.0.1](https://github.com/godotengine/godot/releases/tag/4.0.1-stable) and [scons](https://scons.org/) for your system.
+1. Install [Godot 4.0.2](https://github.com/godotengine/godot/releases/tag/4.0.2-stable) and [scons](https://scons.org/) for your system.
2. Run the command `git submodule update --init --recursive` to retrieve all related submodules.
3. Run `scons` in the project root, you should see a libopenvic2 file in `game/bin/openvic2`.
-4. Open with Godot 4.0.1, click import and navigate to the `game` directory.
+4. Open with Godot 4, click import and navigate to the `game` directory.
5. Import and edit.
6. Once loaded, click the play button at the top right, if you see `Hello GDExtension Singleton!` in the output at the bottom then it is working.
## Project Export
1. Build the extension with `scons` or `scons target=template_debug`. (or `scons target=template_release` for release)
-2. Open `game/project.godot` with Godot 4.0.1.
+2. Open `game/project.godot` with Godot 4.
3. Click `Project` at the top left, click `Export`.
4. If you do not have the templates, you must download the templates, there is highlighted white text at the bottom of the Export subwindow that opens up the template manager for you to download.
5. Click `Export All`:
diff --git a/SConstruct b/SConstruct
index 4e6fe70..3213efd 100644
--- a/SConstruct
+++ b/SConstruct
@@ -10,6 +10,12 @@ ARGUMENTS.pop('intermediate_delete', True)
env = SConscript("godot-cpp/SConstruct")
+# Require C++20
+if env.get("is_msvc", False):
+ env.Replace(CXXFLAGS=["/std:c++20"])
+else:
+ env.Replace(CXXFLAGS=["-std=c++20"])
+
ARGUMENTS = SAVED_ARGUMENTS
# Custom options and profile flags.
diff --git a/docs/general-program-architecture.md b/docs/general-program-architecture.md
index 533a8fa..c37d390 100644
--- a/docs/general-program-architecture.md
+++ b/docs/general-program-architecture.md
@@ -19,14 +19,14 @@ Player ->> UI: Press "New Game" or<br>"Load Game" button
UI ->> Bridge: Begin new Game Session
Bridge ->> Simulation: Start new Game Session
Simulation -->> Dataloader: Load previous savegame<br>(If necessary)
-Dataloader -->> Simulation:
+Dataloader -->> Simulation:
Simulation ->> Bridge: Provide information necessary<br>for UI and visual elements
Bridge ->> UI: Signal that Game Session<br> is ready for interaction
UI ->> Player: Present to Player
loop Core Game Loop
Player ->> UI: Interact with game controls
-UI ->> Bridge: Convey player intent<br>according to UI<br>handler functions
+UI ->> Bridge: Convey player intent<br>according to UI<br>handler functions
Bridge ->> Simulation: Relay changes to entities<br>controlled by the Player
Note over Simulation: When unpaused:
Simulation ->> Simulation: Advance to next in-game day<br>according to game speed<br>and update Simulation state
diff --git a/docs/simulation/goods.md b/docs/simulation/goods.md
index 15b4864..f8204b0 100644
--- a/docs/simulation/goods.md
+++ b/docs/simulation/goods.md
@@ -28,7 +28,7 @@ classDiagram
Good o-- GoodCategory
```
-## Data
+## Data
### Vanilla
diff --git a/docs/simulation/ideologies.md b/docs/simulation/ideologies.md
index f56cda7..817bdb3 100644
--- a/docs/simulation/ideologies.md
+++ b/docs/simulation/ideologies.md
@@ -27,7 +27,7 @@ classDiagram
```
-## Data
+## Data
### Vanilla
@@ -41,7 +41,7 @@ classDiagram
|communist|socialist|1865/01/01|False|#960A0A|
|fascist|fascist|1905/01/01|False|#3C3C3C|
-### John Cena
+### John Cena
|Identifier|Ideological Group|Earliest Date|Uncivilized Nations|Colour|
|--|--|--|--|--|
diff --git a/docs/simulation/provinces.md b/docs/simulation/provinces.md
index 88eea50..642ff11 100644
--- a/docs/simulation/provinces.md
+++ b/docs/simulation/provinces.md
@@ -5,16 +5,16 @@
```mermaid
classDiagram
class Province {
- Int64 id
- string provinceName
- string tradeGood
- Int64 lifeRating
- bool hasLegalSlavery
+ Int64 id
+ string provinceName
+ string tradeGood
+ Int64 lifeRating
+ bool hasLegalSlavery
- Int64 fortLevel
- Int64 navalBaseLevel
+ Int64 fortLevel
+ Int64 navalBaseLevel
Int64 railroadLevel
-
+
string regionId
string continentId
}
diff --git a/docs/styleguide-cpp.md b/docs/styleguide-cpp.md
index 69471f5..6440314 100644
--- a/docs/styleguide-cpp.md
+++ b/docs/styleguide-cpp.md
@@ -1,4 +1,4 @@
-# OpenVic2 C++ Style Guidelines
+# OpenVic2 C++ Style Guidelines
## Table of Contents
1. [Why Style?](styleguide-cpp.md#1-why-style)
@@ -169,7 +169,7 @@ Source code files should adhere to the following:
longArgumentNameOne, longArgumentNameTwo, longArgumentNameThree,
longArgumentNameFour, longArgumentNameFive, longArgumentNameSix,
longArgumentNameSeven, longArgumentNameEight);
-
+
//Incorrect
callingAFunctionWithAVeryLongNameAndManyArguments(longArgumentNameOne, longArgumentNameTwo, longArgumentNameThree, longArgumentNameFour, longArgumentNameFive, longArgumentNameSix, longArgumentNameSeven, longArgumentNameEight);
```
@@ -255,7 +255,7 @@ Source code files should adhere to the following:
- For constant values that are computable at compile-time, use `constexpr` instead of the `const` keyword
```c++
//Correct
- constexpr size_t UNIQUE_RGB_COLOURS = 256 * 256 * 256;
+ constexpr size_t UNIQUE_RGB_COLOURS = 256 * 256 * 256;
//Incorrect
const size_t UNIQUE_RGB_COLOURS = 256 * 256 * 256;
diff --git a/extension/src/Checksum.hpp b/extension/src/Checksum.hpp
index ebdbd43..717910e 100644
--- a/extension/src/Checksum.hpp
+++ b/extension/src/Checksum.hpp
@@ -1,15 +1,13 @@
#pragma once
-#include <godot_cpp/classes/object.hpp>
#include <godot_cpp/core/class_db.hpp>
-#include <godot_cpp/variant/utility_functions.hpp>
namespace OpenVic2 {
class Checksum : public godot::Object {
GDCLASS(Checksum, godot::Object)
//BEGIN BOILERPLATE
- static Checksum* _checksum;
+ inline static Checksum* _checksum = nullptr;
protected:
static void _bind_methods() {
@@ -33,6 +31,4 @@ namespace OpenVic2 {
return godot::String("1234abcd");
}
};
-
- Checksum* Checksum::_checksum = nullptr;
-} \ No newline at end of file
+}
diff --git a/extension/src/GameSingleton.cpp b/extension/src/GameSingleton.cpp
new file mode 100644
index 0000000..3811dea
--- /dev/null
+++ b/extension/src/GameSingleton.cpp
@@ -0,0 +1,463 @@
+#include "GameSingleton.hpp"
+
+#include <godot_cpp/variant/utility_functions.hpp>
+#include <godot_cpp/classes/file_access.hpp>
+#include <godot_cpp/classes/json.hpp>
+
+#include "openvic2/Logger.hpp"
+
+using namespace godot;
+using namespace OpenVic2;
+
+#define ERR(x) ((x) == SUCCESS ? OK : FAILED)
+
+GameSingleton* GameSingleton::singleton = nullptr;
+
+void GameSingleton::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("load_province_identifier_file", "file_path"), &GameSingleton::load_province_identifier_file);
+ ClassDB::bind_method(D_METHOD("load_water_province_file", "file_path"), &GameSingleton::load_water_province_file);
+ ClassDB::bind_method(D_METHOD("load_region_file", "file_path"), &GameSingleton::load_region_file);
+ ClassDB::bind_method(D_METHOD("load_province_shape_file", "file_path"), &GameSingleton::load_province_shape_file);
+ ClassDB::bind_method(D_METHOD("setup"), &GameSingleton::setup);
+
+ ClassDB::bind_method(D_METHOD("get_province_index_from_uv_coords", "coords"), &GameSingleton::get_province_index_from_uv_coords);
+ ClassDB::bind_method(D_METHOD("get_province_info_from_index", "index"), &GameSingleton::get_province_info_from_index);
+ ClassDB::bind_method(D_METHOD("get_width"), &GameSingleton::get_width);
+ ClassDB::bind_method(D_METHOD("get_height"), &GameSingleton::get_height);
+ ClassDB::bind_method(D_METHOD("get_province_index_images"), &GameSingleton::get_province_index_images);
+ ClassDB::bind_method(D_METHOD("get_province_colour_image"), &GameSingleton::get_province_colour_image);
+
+ ClassDB::bind_method(D_METHOD("update_colour_image"), &GameSingleton::update_colour_image);
+ ClassDB::bind_method(D_METHOD("get_mapmode_count"), &GameSingleton::get_mapmode_count);
+ ClassDB::bind_method(D_METHOD("get_mapmode_identifier", "index"), &GameSingleton::get_mapmode_identifier);
+ ClassDB::bind_method(D_METHOD("set_mapmode", "identifier"), &GameSingleton::set_mapmode);
+
+ ClassDB::bind_method(D_METHOD("expand_building", "province_index", "building_type_identifier"), &GameSingleton::expand_building);
+
+ ClassDB::bind_method(D_METHOD("set_paused", "paused"), &GameSingleton::set_paused);
+ ClassDB::bind_method(D_METHOD("toggle_paused"), &GameSingleton::toggle_paused);
+ ClassDB::bind_method(D_METHOD("is_paused"), &GameSingleton::is_paused);
+ ClassDB::bind_method(D_METHOD("increase_speed"), &GameSingleton::increase_speed);
+ ClassDB::bind_method(D_METHOD("decrease_speed"), &GameSingleton::decrease_speed);
+ ClassDB::bind_method(D_METHOD("can_increase_speed"), &GameSingleton::can_increase_speed);
+ ClassDB::bind_method(D_METHOD("can_decrease_speed"), &GameSingleton::can_decrease_speed);
+ ClassDB::bind_method(D_METHOD("get_longform_date"), &GameSingleton::get_longform_date);
+ ClassDB::bind_method(D_METHOD("try_tick"), &GameSingleton::try_tick);
+
+ ADD_SIGNAL(MethodInfo("state_updated"));
+}
+
+GameSingleton* GameSingleton::get_singleton() {
+ return singleton;
+}
+
+/* REQUIREMENTS:
+ * MAP-21, MAP-25
+ */
+GameSingleton::GameSingleton() : game_manager{ [this]() { emit_signal("state_updated"); } } {
+ ERR_FAIL_COND(singleton != nullptr);
+ singleton = this;
+
+ Logger::set_info_func([](std::string&& str) { UtilityFunctions::print(str.c_str()); });
+ Logger::set_error_func([](std::string&& str) { UtilityFunctions::push_error(str.c_str()); });
+
+ using mapmode_t = std::pair<std::string, Mapmode::colour_func_t>;
+ const std::vector<mapmode_t> mapmodes = {
+ { "mapmode_province", [](Map const&, Province const& province) -> Province::colour_t { return province.get_colour(); } },
+ { "mapmode_region", [](Map const&, Province const& province) -> Province::colour_t {
+ Region const* region = province.get_region();
+ if (region != nullptr) return region->get_colour();
+ return province.get_colour();
+ } },
+ { "mapmode_terrain", [](Map const&, Province const& province) -> Province::colour_t {
+ return province.is_water() ? 0x4287F5 : 0x0D7017;
+ } },
+ { "mapmode_index", [](Map const& map, Province const& province) -> Province::colour_t {
+ const uint8_t f = static_cast<float>(province.get_index()) / static_cast<float>(map.get_province_count()) * 255.0f;
+ return (f << 16) | (f << 8) | f;
+ } }
+ };
+ for (mapmode_t const& mapmode : mapmodes)
+ game_manager.map.add_mapmode(mapmode.first, mapmode.second);
+ game_manager.map.lock_mapmodes();
+
+ using building_type_t = std::tuple<std::string, Building::level_t, Timespan>;
+ const std::vector<building_type_t> building_types = {
+ { "building_fort", 4, 8 }, { "building_naval_base", 6, 15 }, { "building_railroad", 5, 10 }
+ };
+ for (building_type_t const& type : building_types)
+ game_manager.building_manager.add_building_type(std::get<0>(type), std::get<1>(type), std::get<2>(type));
+ game_manager.building_manager.lock_building_types();
+
+}
+
+GameSingleton::~GameSingleton() {
+ ERR_FAIL_COND(singleton != this);
+ singleton = nullptr;
+}
+
+static Error load_json_file(String const& file_description, String const& file_path, Variant& result) {
+ result.clear();
+ UtilityFunctions::print("Loading ", file_description, " file: ", file_path);
+ const Ref<FileAccess> file = FileAccess::open(file_path, FileAccess::ModeFlags::READ);
+ Error err = FileAccess::get_open_error();
+ if (err != OK || file.is_null()) {
+ UtilityFunctions::push_error("Failed to load ", file_description, " file: ", file_path);
+ return err == OK ? FAILED : err;
+ }
+ const String json_string = file->get_as_text();
+ JSON json;
+ err = json.parse(json_string);
+ if (err != OK) {
+ UtilityFunctions::push_error("Failed to parse ", file_description, " file as JSON: ", file_path,
+ "\nError at line ", json.get_error_line(), ": ", json.get_error_message());
+ return err;
+ }
+ result = json.get_data();
+ return err;
+}
+
+using parse_json_entry_func_t = std::function<godot::Error (godot::String const&, godot::Variant const&)>;
+
+static Error parse_json_dictionary_file(String const& file_description, String const& file_path,
+ String const& identifier_prefix, parse_json_entry_func_t parse_entry) {
+ Variant json_var;
+ Error err = load_json_file(file_description, file_path, json_var);
+ if (err != OK) return err;
+ const Variant::Type type = json_var.get_type();
+ if (type != Variant::DICTIONARY) {
+ UtilityFunctions::push_error("Invalid ", file_description, " JSON: root has type ",
+ Variant::get_type_name(type), " (expected Dictionary)");
+ return FAILED;
+ }
+ Dictionary const& dict = json_var;
+ const Array identifiers = dict.keys();
+ for (int64_t idx = 0; idx < identifiers.size(); ++idx) {
+ String const& identifier = identifiers[idx];
+ Variant const& entry = dict[identifier];
+ if (identifier.is_empty()) {
+ UtilityFunctions::push_error("Empty identifier in ", file_description, " file with entry: ", entry);
+ err = FAILED;
+ continue;
+ }
+ if (!identifier.begins_with(identifier_prefix))
+ UtilityFunctions::push_warning("Identifier in ", file_description, " file missing \"", identifier_prefix, "\" prefix: ", identifier);
+ if (parse_entry(identifier, entry) != OK) err = FAILED;
+ }
+ return err;
+}
+
+Error GameSingleton::_parse_province_identifier_entry(String const& identifier, Variant const& entry) {
+ const Variant::Type type = entry.get_type();
+ Province::colour_t colour = Province::NULL_COLOUR;
+ if (type == Variant::ARRAY) {
+ Array const& colour_array = entry;
+ if (colour_array.size() == 3) {
+ for (int jdx = 0; jdx < 3; ++jdx) {
+ Variant const& var = colour_array[jdx];
+ if (var.get_type() != Variant::FLOAT) {
+ colour = Province::NULL_COLOUR;
+ break;
+ }
+ const double colour_double = var;
+ if (std::trunc(colour_double) != colour_double) {
+ colour = Province::NULL_COLOUR;
+ break;
+ }
+ const int64_t colour_int = static_cast<int64_t>(colour_double);
+ if (colour_int < 0 || colour_int > 255) {
+ colour = Province::NULL_COLOUR;
+ break;
+ }
+ colour = (colour << 8) | colour_int;
+ }
+ }
+ } else if (type == Variant::STRING) {
+ String const& colour_string = entry;
+ if (colour_string.is_valid_hex_number()) {
+ const int64_t colour_int = colour_string.hex_to_int();
+ if (0 <= colour_int && colour_int <= 0xFFFFFF)
+ colour = colour_int;
+ }
+ }
+ if (colour == Province::NULL_COLOUR) {
+ UtilityFunctions::push_error("Invalid colour for province identifier \"", identifier, "\": ", entry);
+ return FAILED;
+ }
+ return ERR(game_manager.map.add_province(identifier.utf8().get_data(), colour));
+}
+
+Error GameSingleton::load_province_identifier_file(String const& file_path) {
+ const Error err = parse_json_dictionary_file("province identifier", file_path, "prov_",
+ [this](String const& identifier, Variant const& entry) -> Error {
+ return _parse_province_identifier_entry(identifier, entry);
+ });
+ game_manager.map.lock_provinces();
+ return err;
+}
+
+Error GameSingleton::_parse_region_entry(String const& identifier, Variant const& entry) {
+ Error err = OK;
+ Variant::Type type = entry.get_type();
+ std::vector<std::string> province_identifiers;
+ if (type == Variant::ARRAY) {
+ Array const& province_array = entry;
+ for (int64_t idx = 0; idx < province_array.size(); ++idx) {
+ Variant const& province_var = province_array[idx];
+ type = province_var.get_type();
+ if (type == Variant::STRING) {
+ String const& province_string = province_var;
+ province_identifiers.push_back(province_string.utf8().get_data());
+ } else {
+ UtilityFunctions::push_error("Invalid province identifier for region \"", identifier, "\": ", entry);
+ err = FAILED;
+ }
+ }
+ }
+ if (province_identifiers.empty()) {
+ UtilityFunctions::push_error("Invalid province list for region \"", identifier, "\": ", entry);
+ return FAILED;
+ }
+ return ERR(game_manager.map.add_region(identifier.utf8().get_data(), province_identifiers));
+}
+
+Error GameSingleton::load_region_file(String const& file_path) {
+ const Error err = parse_json_dictionary_file("region", file_path, "region_",
+ [this](String const& identifier, Variant const& entry) -> Error {
+ return _parse_region_entry(identifier, entry);
+ });
+ game_manager.map.lock_regions();
+ return err;
+}
+
+Error GameSingleton::load_province_shape_file(String const& file_path) {
+ if (province_index_image[0].is_valid()) {
+ UtilityFunctions::push_error("Province shape file has already been loaded, cannot load: ", file_path);
+ return FAILED;
+ }
+ Ref<Image> province_shape_image;
+ province_shape_image.instantiate();
+ Error err = province_shape_image->load(file_path);
+ if (err != OK) {
+ UtilityFunctions::push_error("Failed to load province shape file: ", file_path);
+ return err;
+ }
+ const int32_t width = province_shape_image->get_width();
+ const int32_t height = province_shape_image->get_height();
+ if (width < 1 || height < 1) {
+ UtilityFunctions::push_error("Invalid dimensions (", width, "x", height, ") for province shape file: ", file_path);
+ err = FAILED;
+ }
+ if (width % image_width_divide != 0) {
+ UtilityFunctions::push_error("Invalid width ", width, " (must be divisible by ", image_width_divide, ") for province shape file: ", file_path);
+ err = FAILED;
+ }
+ static constexpr Image::Format expected_format = Image::FORMAT_RGB8;
+ const Image::Format format = province_shape_image->get_format();
+ if (format != expected_format) {
+ UtilityFunctions::push_error("Invalid format (", format, ", should be ", expected_format, ") for province shape file: ", file_path);
+ err = FAILED;
+ }
+ if (err != OK) return err;
+ err = ERR(game_manager.map.generate_province_index_image(width, height, province_shape_image->get_data().ptr()));
+
+ std::vector<Province::index_t> const& province_index_data = game_manager.map.get_province_index_image();
+ const int32_t divided_width = width / image_width_divide;
+ for (int32_t i = 0; i < image_width_divide; ++i) {
+ PackedByteArray index_data_array;
+ index_data_array.resize(divided_width * height * sizeof(Province::index_t));
+ for (int32_t y = 0; y < height; ++y)
+ memcpy(index_data_array.ptrw() + y * divided_width * sizeof(Province::index_t),
+ province_index_data.data() + y * width + i * divided_width,
+ divided_width * sizeof(Province::index_t));
+ province_index_image[i] = Image::create_from_data(divided_width, height, false, Image::FORMAT_RG8, index_data_array);
+ if (province_index_image[i].is_null()) {
+ UtilityFunctions::push_error("Failed to create province ID image #", i);
+ err = FAILED;
+ }
+ }
+
+ if (update_colour_image() != OK) err = FAILED;
+
+ return err;
+}
+
+godot::Error GameSingleton::setup() {
+ return ERR(game_manager.setup());
+}
+
+Error GameSingleton::load_water_province_file(String const& file_path) {
+ Variant json_var;
+ Error err = load_json_file("water province", file_path, json_var);
+ if (err != OK) return err;
+ Variant::Type type = json_var.get_type();
+ if (type != Variant::ARRAY) {
+ UtilityFunctions::push_error("Invalid water province JSON: root has type ",
+ Variant::get_type_name(type), " (expected Array)");
+ err = FAILED;
+ } else {
+ Array const& array = json_var;
+ for (int64_t idx = 0; idx < array.size(); ++idx) {
+ Variant const& entry = array[idx];
+ type = entry.get_type();
+ if (type != Variant::STRING) {
+ UtilityFunctions::push_error("Invalid water province identifier: ", entry);
+ err = FAILED;
+ continue;
+ }
+ String const& identifier = entry;
+ if (game_manager.map.set_water_province(identifier.utf8().get_data()) != SUCCESS)
+ err = FAILED;
+ }
+ }
+ game_manager.map.lock_water_provinces();
+ return err;
+}
+
+int32_t GameSingleton::get_province_index_from_uv_coords(Vector2 const& coords) const {
+ const size_t x_mod_w = UtilityFunctions::fposmod(coords.x, 1.0f) * get_width();
+ const size_t y_mod_h = UtilityFunctions::fposmod(coords.y, 1.0f) * get_height();
+ return game_manager.map.get_province_index_at(x_mod_w, y_mod_h);
+}
+
+#define KEY(x) static const StringName x##_key = #x;
+Dictionary GameSingleton::get_province_info_from_index(int32_t index) const {
+ Province const* province = game_manager.map.get_province_by_index(index);
+ if (province == nullptr) return {};
+ KEY(province) KEY(region) KEY(life_rating) KEY(buildings)
+ Dictionary ret;
+
+ ret[province_key] = province->get_identifier().c_str();
+
+ Region const* region = province->get_region();
+ if (region != nullptr) ret[region_key] = region->get_identifier().c_str();
+
+ ret[life_rating_key] = province->get_life_rating();
+
+ std::vector<Building> const& buildings = province->get_buildings();
+ if (!buildings.empty()) {
+ Array buildings_array;
+ buildings_array.resize(buildings.size());
+ for (size_t idx = 0; idx < buildings.size(); ++idx) {
+ KEY(building) KEY(level) KEY(expansion_state) KEY(start_date) KEY(end_date) KEY(expansion_progress)
+
+ Dictionary building_dict;
+ Building const& building = buildings[idx];
+ building_dict[building_key] = building.get_identifier().c_str();
+ building_dict[level_key] = static_cast<int32_t>(building.get_level());
+ building_dict[expansion_state_key] = static_cast<int32_t>(building.get_expansion_state());
+ building_dict[start_date_key] = static_cast<std::string>(building.get_start_date()).c_str();
+ building_dict[end_date_key] = static_cast<std::string>(building.get_end_date()).c_str();
+ building_dict[expansion_progress_key] = building.get_expansion_progress();
+
+ buildings_array[idx] = building_dict;
+ }
+ ret[buildings_key] = buildings_array;
+ }
+ return ret;
+}
+#undef KEY
+
+int32_t GameSingleton::get_width() const {
+ return game_manager.map.get_width();
+}
+
+int32_t GameSingleton::get_height() const {
+ return game_manager.map.get_height();
+}
+
+Array GameSingleton::get_province_index_images() const {
+ Array ret;
+ for (int i = 0; i < image_width_divide; ++i)
+ ret.append(province_index_image[i]);
+ return ret;
+}
+
+Ref<Image> GameSingleton::get_province_colour_image() const {
+ return province_colour_image;
+}
+
+Error GameSingleton::update_colour_image() {
+ static PackedByteArray colour_data_array;
+ static constexpr int64_t colour_data_array_size = (Province::MAX_INDEX + 1) * 4;
+ colour_data_array.resize(colour_data_array_size);
+
+ Error err = OK;
+ if (game_manager.map.generate_mapmode_colours(mapmode_index, colour_data_array.ptrw()) != SUCCESS)
+ err = FAILED;
+
+ static constexpr int32_t PROVINCE_INDEX_SQRT = 1 << (sizeof(Province::index_t) * 4);
+ if (province_colour_image.is_null())
+ province_colour_image.instantiate();
+ province_colour_image->set_data(PROVINCE_INDEX_SQRT, PROVINCE_INDEX_SQRT,
+ false, Image::FORMAT_RGBA8, colour_data_array);
+ if (province_colour_image.is_null()) {
+ UtilityFunctions::push_error("Failed to update province colour image");
+ return FAILED;
+ }
+ return err;
+}
+
+int32_t GameSingleton::get_mapmode_count() const {
+ return game_manager.map.get_mapmode_count();
+}
+
+String GameSingleton::get_mapmode_identifier(int32_t index) const {
+ Mapmode const* mapmode = game_manager.map.get_mapmode_by_index(index);
+ if (mapmode != nullptr) return mapmode->get_identifier().c_str();
+ return String{};
+}
+
+Error GameSingleton::set_mapmode(godot::String const& identifier) {
+ Mapmode const* mapmode = game_manager.map.get_mapmode_by_identifier(identifier.utf8().get_data());
+ if (mapmode == nullptr) {
+ UtilityFunctions::push_error("Failed to set mapmode to: ", identifier);
+ return FAILED;
+ }
+ mapmode_index = mapmode->get_index();
+ return OK;
+}
+
+Error GameSingleton::expand_building(int32_t province_index, String const& building_type_identifier) {
+ if (game_manager.expand_building(province_index, building_type_identifier.utf8().get_data()) != SUCCESS) {
+ UtilityFunctions::push_error("Failed to expand ", building_type_identifier, " at province index ", province_index);
+ return FAILED;
+ }
+ return OK;
+}
+
+void GameSingleton::set_paused(bool paused) {
+ game_manager.clock.isPaused = paused;
+}
+
+void GameSingleton::toggle_paused() {
+ game_manager.clock.isPaused = !game_manager.clock.isPaused;
+}
+
+bool GameSingleton::is_paused() const {
+ return game_manager.clock.isPaused;
+}
+
+void GameSingleton::increase_speed() {
+ game_manager.clock.increaseSimulationSpeed();
+}
+
+void GameSingleton::decrease_speed() {
+ game_manager.clock.decreaseSimulationSpeed();
+}
+
+bool GameSingleton::can_increase_speed() const {
+ return game_manager.clock.canIncreaseSimulationSpeed();
+}
+
+bool GameSingleton::can_decrease_speed() const {
+ return game_manager.clock.canDecreaseSimulationSpeed();
+}
+
+String GameSingleton::get_longform_date() const {
+ return static_cast<std::string>(game_manager.get_today()).c_str();
+}
+
+void GameSingleton::try_tick() {
+ game_manager.clock.conditionallyAdvanceGame();
+}
diff --git a/extension/src/GameSingleton.hpp b/extension/src/GameSingleton.hpp
new file mode 100644
index 0000000..d9879ef
--- /dev/null
+++ b/extension/src/GameSingleton.hpp
@@ -0,0 +1,63 @@
+#pragma once
+
+#include <functional>
+
+#include <godot_cpp/classes/image.hpp>
+
+#include "openvic2/GameManager.hpp"
+
+namespace OpenVic2 {
+ class GameSingleton : public godot::Object {
+ GDCLASS(GameSingleton, godot::Object)
+
+ static GameSingleton* singleton;
+
+ GameManager game_manager;
+
+ static constexpr int image_width_divide = 2;
+ godot::Ref<godot::Image> province_index_image[image_width_divide], province_colour_image;
+ Mapmode::index_t mapmode_index = 0;
+
+ godot::Error _parse_province_identifier_entry(godot::String const& identifier, godot::Variant const& entry);
+ godot::Error _parse_region_entry(godot::String const& identifier, godot::Variant const& entry);
+ void _tick();
+ protected:
+ static void _bind_methods();
+
+ public:
+ static GameSingleton* get_singleton();
+
+ GameSingleton();
+ ~GameSingleton();
+
+ godot::Error load_province_identifier_file(godot::String const& file_path);
+ godot::Error load_water_province_file(godot::String const& file_path);
+ godot::Error load_region_file(godot::String const& file_path);
+ godot::Error load_province_shape_file(godot::String const& file_path);
+ godot::Error setup();
+
+ int32_t get_province_index_from_uv_coords(godot::Vector2 const& coords) const;
+ godot::Dictionary get_province_info_from_index(int32_t index) const;
+ int32_t get_width() const;
+ int32_t get_height() const;
+ godot::Array get_province_index_images() const;
+ godot::Ref<godot::Image> get_province_colour_image() const;
+
+ godot::Error update_colour_image();
+ int32_t get_mapmode_count() const;
+ godot::String get_mapmode_identifier(int32_t index) const;
+ godot::Error set_mapmode(godot::String const& identifier);
+
+ godot::Error expand_building(int32_t province_index, godot::String const& building_type_identifier);
+
+ void set_paused(bool paused);
+ void toggle_paused();
+ bool is_paused() const;
+ void increase_speed();
+ void decrease_speed();
+ bool can_increase_speed() const;
+ bool can_decrease_speed() const;
+ godot::String get_longform_date() const;
+ void try_tick();
+ };
+}
diff --git a/extension/src/LoadLocalisation.cpp b/extension/src/LoadLocalisation.cpp
index c95c08b..8698bb2 100644
--- a/extension/src/LoadLocalisation.cpp
+++ b/extension/src/LoadLocalisation.cpp
@@ -8,7 +8,7 @@
using namespace godot;
using namespace OpenVic2;
-LoadLocalisation *LoadLocalisation::singleton = nullptr;
+LoadLocalisation* LoadLocalisation::singleton = nullptr;
void LoadLocalisation::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_file", "file_path", "locale"), &LoadLocalisation::load_file);
@@ -16,7 +16,7 @@ void LoadLocalisation::_bind_methods() {
ClassDB::bind_method(D_METHOD("load_localisation_dir", "dir_path"), &LoadLocalisation::load_localisation_dir);
}
-LoadLocalisation *LoadLocalisation::get_singleton() {
+LoadLocalisation* LoadLocalisation::get_singleton() {
return singleton;
}
@@ -54,7 +54,7 @@ Error LoadLocalisation::_load_file_into_translation(String const& file_path, Ref
}
Ref<Translation> LoadLocalisation::_get_translation(String const& locale) {
- TranslationServer *server = TranslationServer::get_singleton();
+ TranslationServer* server = TranslationServer::get_singleton();
Ref<Translation> translation = server->get_translation_object(locale);
if (translation.is_null() || translation->get_locale() != locale) {
translation.instantiate();
@@ -93,7 +93,7 @@ Error LoadLocalisation::load_locale_dir(String const& dir_path, String const& lo
*/
Error LoadLocalisation::load_localisation_dir(String const& dir_path) {
if (DirAccess::dir_exists_absolute(dir_path)) {
- TranslationServer *server = TranslationServer::get_singleton();
+ TranslationServer* server = TranslationServer::get_singleton();
Error err = OK;
for (String const& locale_name : DirAccess::get_directories_at(dir_path)) {
if (locale_name == server->standardize_locale(locale_name)) {
diff --git a/extension/src/LoadLocalisation.hpp b/extension/src/LoadLocalisation.hpp
index 90f3158..49c0313 100644
--- a/extension/src/LoadLocalisation.hpp
+++ b/extension/src/LoadLocalisation.hpp
@@ -1,14 +1,13 @@
#pragma once
-#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/classes/translation.hpp>
namespace OpenVic2 {
- class LoadLocalisation : public godot::Object
- {
+ class LoadLocalisation : public godot::Object {
+
GDCLASS(LoadLocalisation, godot::Object)
- static LoadLocalisation *singleton;
+ static LoadLocalisation* singleton;
godot::Error _load_file_into_translation(godot::String const& file_path, godot::Ref<godot::Translation> translation);
godot::Ref<godot::Translation> _get_translation(godot::String const& locale);
@@ -17,7 +16,7 @@ namespace OpenVic2 {
static void _bind_methods();
public:
- static LoadLocalisation *get_singleton();
+ static LoadLocalisation* get_singleton();
LoadLocalisation();
~LoadLocalisation();
diff --git a/extension/src/MapMesh.cpp b/extension/src/MapMesh.cpp
new file mode 100644
index 0000000..91c7611
--- /dev/null
+++ b/extension/src/MapMesh.cpp
@@ -0,0 +1,150 @@
+#include "MapMesh.hpp"
+
+#include <godot_cpp/templates/vector.hpp>
+
+using namespace godot;
+using namespace OpenVic2;
+
+void MapMesh::_bind_methods() {
+ ClassDB::bind_method(D_METHOD("set_aspect_ratio", "ratio"), &MapMesh::set_aspect_ratio);
+ ClassDB::bind_method(D_METHOD("get_aspect_ratio"), &MapMesh::get_aspect_ratio);
+
+ ClassDB::bind_method(D_METHOD("set_repeat_proportion", "proportion"), &MapMesh::set_repeat_proportion);
+ ClassDB::bind_method(D_METHOD("get_repeat_proportion"), &MapMesh::get_repeat_proportion);
+
+ ClassDB::bind_method(D_METHOD("set_subdivide_width", "divisions"), &MapMesh::set_subdivide_width);
+ ClassDB::bind_method(D_METHOD("get_subdivide_width"), &MapMesh::get_subdivide_width);
+
+ ClassDB::bind_method(D_METHOD("set_subdivide_depth", "divisions"), &MapMesh::set_subdivide_depth);
+ ClassDB::bind_method(D_METHOD("get_subdivide_depth"), &MapMesh::get_subdivide_depth);
+
+ ClassDB::bind_method(D_METHOD("get_core_aabb"), &MapMesh::get_core_aabb);
+ ClassDB::bind_method(D_METHOD("is_valid_uv_coord"), &MapMesh::is_valid_uv_coord);
+
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "aspect_ratio", PROPERTY_HINT_NONE, "suffix:m"), "set_aspect_ratio", "get_aspect_ratio");
+ ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "repeat_proportion", PROPERTY_HINT_NONE, "suffix:m"), "set_repeat_proportion", "get_repeat_proportion");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "subdivide_width", PROPERTY_HINT_RANGE, "0,100,1,or_greater"), "set_subdivide_width", "get_subdivide_width");
+ ADD_PROPERTY(PropertyInfo(Variant::INT, "subdivide_depth", PROPERTY_HINT_RANGE, "0,100,1,or_greater"), "set_subdivide_depth", "get_subdivide_depth");
+}
+
+void MapMesh::_request_update() {
+ // Hack to trigger _update_lightmap_size and _request_update in PrimitiveMesh
+ set_add_uv2(get_add_uv2());
+}
+
+void MapMesh::set_aspect_ratio(const float ratio) {
+ aspect_ratio = ratio;
+ _request_update();
+}
+
+float MapMesh::get_aspect_ratio() const {
+ return aspect_ratio;
+}
+
+void MapMesh::set_repeat_proportion(const float proportion) {
+ repeat_proportion = proportion;
+ _request_update();
+}
+
+float MapMesh::get_repeat_proportion() const {
+ return repeat_proportion;
+}
+
+void MapMesh::set_subdivide_width(const int divisions) {
+ subdivide_w = divisions > 0 ? divisions : 0;
+ _request_update();
+}
+
+int MapMesh::get_subdivide_width() const {
+ return subdivide_w;
+}
+
+void MapMesh::set_subdivide_depth(const int divisions) {
+ subdivide_d = divisions > 0 ? divisions : 0;
+ _request_update();
+}
+
+int MapMesh::get_subdivide_depth() const {
+ return subdivide_d;
+}
+
+AABB MapMesh::get_core_aabb() const {
+ const Vector3 size{ aspect_ratio, 0.0f, 1.0f };
+ return AABB{ size * -0.5f, size };
+}
+
+bool MapMesh::is_valid_uv_coord(godot::Vector2 const& uv) const {
+ return 0.0f <= uv.y && uv.y <= 1.0f;
+}
+
+Array MapMesh::_create_mesh_array() const {
+ Array arr;
+ arr.resize(Mesh::ARRAY_MAX);
+
+ const int vertex_count = (subdivide_w + 2) * (subdivide_d + 2);
+ const int indice_count = (subdivide_w + 1) * (subdivide_d + 1) * 6;
+
+ PackedVector3Array points;
+ PackedVector3Array normals;
+ PackedFloat32Array tangents;
+ PackedVector2Array uvs;
+ PackedInt32Array indices;
+
+ points.resize(vertex_count);
+ normals.resize(vertex_count);
+ tangents.resize(vertex_count * 4);
+ uvs.resize(vertex_count);
+ indices.resize(indice_count);
+
+ static const Vector3 normal{ 0.0f, 1.0f, 0.0f };
+ const Size2 uv_size{ 1.0f + 2.0f * repeat_proportion, 1.0f };
+ const Size2 size{ aspect_ratio * uv_size.x, uv_size.y }, start_pos = size * -0.5f;
+
+ int point_index = 0, thisrow = 0, prevrow = 0, indice_index = 0;
+ Vector2 subdivide_step{ 1.0f / (subdivide_w + 1.0f) , 1.0f / (subdivide_d + 1.0f) };
+ Vector3 point{ 0.0f, 0.0f, start_pos.y };
+ Vector2 point_step = subdivide_step * size;
+ Vector2 uv{}, uv_step = subdivide_step * uv_size;
+
+ for (int j = 0; j <= subdivide_d + 1; ++j) {
+ point.x = start_pos.x;
+ uv.x = -repeat_proportion;
+
+ for (int i = 0; i <= subdivide_w + 1; ++i) {
+ points[point_index] = point;
+ normals[point_index] = normal;
+ tangents[point_index * 4 + 0] = 1.0f;
+ tangents[point_index * 4 + 1] = 0.0f;
+ tangents[point_index * 4 + 2] = 0.0f;
+ tangents[point_index * 4 + 3] = 1.0f;
+ uvs[point_index] = uv;
+ point_index++;
+
+ if (i > 0 && j > 0) {
+ indices[indice_index + 0] = prevrow + i - 1;
+ indices[indice_index + 1] = prevrow + i;
+ indices[indice_index + 2] = thisrow + i - 1;
+ indices[indice_index + 3] = prevrow + i;
+ indices[indice_index + 4] = thisrow + i;
+ indices[indice_index + 5] = thisrow + i - 1;
+ indice_index += 6;
+ }
+
+ point.x += point_step.x;
+ uv.x += uv_step.x;
+ }
+
+ point.z += point_step.y;
+ uv.y += uv_step.y;
+ prevrow = thisrow;
+ thisrow = point_index;
+ }
+
+ arr[Mesh::ARRAY_VERTEX] = points;
+ arr[Mesh::ARRAY_NORMAL] = normals;
+ arr[Mesh::ARRAY_TANGENT] = tangents;
+ arr[Mesh::ARRAY_TEX_UV] = uvs;
+ arr[Mesh::ARRAY_INDEX] = indices;
+
+ return arr;
+}
diff --git a/extension/src/MapMesh.hpp b/extension/src/MapMesh.hpp
new file mode 100644
index 0000000..d8727cf
--- /dev/null
+++ b/extension/src/MapMesh.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <godot_cpp/classes/primitive_mesh.hpp>
+
+namespace OpenVic2 {
+ class MapMesh : public godot::PrimitiveMesh {
+ GDCLASS(MapMesh, godot::PrimitiveMesh)
+
+ float aspect_ratio = 2.0f, repeat_proportion = 0.5f;
+ int subdivide_w = 0, subdivide_d = 0;
+
+ protected:
+ static void _bind_methods();
+ void _request_update();
+
+ public:
+ void set_aspect_ratio(const float ratio);
+ float get_aspect_ratio() const;
+
+ void set_repeat_proportion(const float proportion);
+ float get_repeat_proportion() const;
+
+ void set_subdivide_width(const int divisions);
+ int get_subdivide_width() const;
+
+ void set_subdivide_depth(const int divisions);
+ int get_subdivide_depth() const;
+
+ godot::AABB get_core_aabb() const;
+ bool is_valid_uv_coord(godot::Vector2 const& uv) const;
+
+ godot::Array _create_mesh_array() const override;
+ };
+}
diff --git a/extension/src/Simulation.hpp b/extension/src/Simulation.hpp
deleted file mode 100644
index 8959310..0000000
--- a/extension/src/Simulation.hpp
+++ /dev/null
@@ -1,52 +0,0 @@
-#pragma once
-
-#include <godot_cpp/classes/object.hpp>
-#include <godot_cpp/core/class_db.hpp>
-#include <godot_cpp/variant/utility_functions.hpp>
-#include <vector>
-
-namespace OpenVic2 {
- class Simulation : public godot::Object {
- GDCLASS(Simulation, godot::Object)
- std::vector<uint64_t> exampleProvinces;
-
- //BEGIN BOILERPLATE
- static Simulation* _simulation;
-
- protected:
- static void _bind_methods() {
- godot::ClassDB::bind_method(godot::D_METHOD("conductSimulationStep"), &Simulation::conductSimulationStep);
- godot::ClassDB::bind_method(godot::D_METHOD("queryProvinceSize"), &Simulation::queryProvinceSize);
- }
-
- public:
- inline static Simulation* get_singleton() { return _simulation; }
-
- inline Simulation() {
- ERR_FAIL_COND(_simulation != nullptr);
- _simulation = this;
-
- exampleProvinces.resize(10, 1);
- }
- inline ~Simulation() {
- ERR_FAIL_COND(_simulation != this);
- _simulation = nullptr;
- }
- //END BOILERPLATE
-
- inline void conductSimulationStep() {
- for (uint64_t x = 0; x < exampleProvinces.size(); x++) {
- exampleProvinces[x] += (x + 1);
- }
- }
-
- inline uint64_t queryProvinceSize(uint64_t provinceID) {
- if (provinceID >= exampleProvinces.size()) {
- return 0;
- }
- return exampleProvinces[provinceID];
- }
- };
-
- Simulation* Simulation::_simulation = nullptr;
-}
diff --git a/extension/src/TestSingleton.cpp b/extension/src/TestSingleton.cpp
deleted file mode 100644
index 0855a30..0000000
--- a/extension/src/TestSingleton.cpp
+++ /dev/null
@@ -1,36 +0,0 @@
-#include "TestSingleton.hpp"
-
-#include <godot_cpp/core/class_db.hpp>
-#include <godot_cpp/variant/utility_functions.hpp>
-
-using namespace godot;
-using namespace OpenVic2;
-
-TestSingleton *TestSingleton::singleton = nullptr;
-
-void TestSingleton::_bind_methods()
-{
- ClassDB::bind_method(D_METHOD("hello_singleton"), &TestSingleton::hello_singleton);
-}
-
-TestSingleton *TestSingleton::get_singleton()
-{
- return singleton;
-}
-
-TestSingleton::TestSingleton()
-{
- ERR_FAIL_COND(singleton != nullptr);
- singleton = this;
-}
-
-TestSingleton::~TestSingleton()
-{
- ERR_FAIL_COND(singleton != this);
- singleton = nullptr;
-}
-
-void TestSingleton::hello_singleton()
-{
- UtilityFunctions::print("Hello GDExtension Singleton!");
-} \ No newline at end of file
diff --git a/extension/src/TestSingleton.hpp b/extension/src/TestSingleton.hpp
deleted file mode 100644
index de27589..0000000
--- a/extension/src/TestSingleton.hpp
+++ /dev/null
@@ -1,24 +0,0 @@
-#pragma once
-
-#include <godot_cpp/classes/object.hpp>
-#include <godot_cpp/core/class_db.hpp>
-
-namespace OpenVic2 {
- class TestSingleton : public godot::Object
- {
- GDCLASS(TestSingleton, godot::Object)
-
- static TestSingleton *singleton;
-
- protected:
- static void _bind_methods();
-
- public:
- static TestSingleton *get_singleton();
-
- TestSingleton();
- ~TestSingleton();
-
- void hello_singleton();
- };
-} \ No newline at end of file
diff --git a/extension/src/openvic2/Date.cpp b/extension/src/openvic2/Date.cpp
new file mode 100644
index 0000000..bb891fd
--- /dev/null
+++ b/extension/src/openvic2/Date.cpp
@@ -0,0 +1,161 @@
+#include "openvic2/Date.hpp"
+
+#include <cctype>
+#include <algorithm>
+
+#include "openvic2/Logger.hpp"
+
+using namespace OpenVic2;
+
+Timespan::Timespan(day_t value) : days{value} {}
+
+bool Timespan::operator<(Timespan other) const { return days < other.days; };
+bool Timespan::operator>(Timespan other) const { return days > other.days; };
+bool Timespan::operator<=(Timespan other) const { return days <= other.days; };
+bool Timespan::operator>=(Timespan other) const { return days >= other.days; };
+bool Timespan::operator==(Timespan other) const { return days == other.days; };
+bool Timespan::operator!=(Timespan other) const { return days != other.days; };
+
+Timespan Timespan::operator+(Timespan other) const { return days + other.days; }
+
+Timespan Timespan::operator-(Timespan other) const { return days - other.days; }
+
+Timespan Timespan::operator*(day_t factor) const { return days * factor; }
+
+Timespan Timespan::operator/(day_t factor) const { return days / factor; }
+
+Timespan& Timespan::operator+=(Timespan other) {
+ days += other.days;
+ return *this;
+}
+
+Timespan& Timespan::operator-=(Timespan other) {
+ days -= other.days;
+ return *this;
+}
+
+Timespan& Timespan::operator++() {
+ days++;
+ return *this;
+}
+
+Timespan Timespan::operator++(int) {
+ Timespan old = *this;
+ ++(*this);
+ return old;
+}
+
+Timespan::operator day_t() const {
+ return days;
+}
+
+Timespan::operator double() const {
+ return days;
+}
+
+Timespan::operator std::string() const {
+ return std::to_string(days);
+}
+
+std::ostream& OpenVic2::operator<<(std::ostream& out, Timespan timespan) {
+ return out << static_cast<std::string>(timespan);
+}
+
+Timespan Date::_dateToTimespan(year_t year, month_t month, day_t day) {
+ month = std::clamp<month_t>(month, 1, MONTHS_IN_YEAR);
+ day = std::clamp<day_t>(day, 1, DAYS_IN_MONTH[month - 1]);
+ return year * DAYS_IN_YEAR + DAYS_UP_TO_MONTH[month - 1] + day - 1;
+}
+
+Date::Date(Timespan total_days) : timespan{ total_days } {
+ if (timespan < 0) {
+ Logger::error("Invalid timespan for date: ", timespan, " (cannot be negative)");
+ timespan = 0;
+ }
+}
+
+Date::Date(year_t year, month_t month, day_t day) : timespan{ _dateToTimespan(year, month, day) } {}
+
+Date::year_t Date::getYear() const {
+ return static_cast<Timespan::day_t>(timespan) / DAYS_IN_YEAR;
+}
+
+Date::month_t Date::getMonth() const {
+ return ((static_cast<Timespan::day_t>(timespan) % DAYS_IN_YEAR) / 32) + 1;
+}
+
+Date::day_t Date::getDay() const {
+ const Timespan::day_t days_in_year = static_cast<Timespan::day_t>(timespan) % DAYS_IN_YEAR;
+ return days_in_year - DAYS_UP_TO_MONTH[days_in_year / 32] + 1;
+}
+
+
+bool Date::operator<(Date other) const { return timespan < other.timespan; };
+bool Date::operator>(Date other) const { return timespan > other.timespan; };
+bool Date::operator<=(Date other) const { return timespan <= other.timespan; };
+bool Date::operator>=(Date other) const { return timespan >= other.timespan; };
+bool Date::operator==(Date other) const { return timespan == other.timespan; };
+bool Date::operator!=(Date other) const { return timespan != other.timespan; };
+
+Date Date::operator+(Timespan other) const { return timespan + other; }
+
+Timespan Date::operator-(Date other) const { return timespan - other.timespan; }
+
+Date& Date::operator+=(Timespan other) {
+ timespan += other;
+ return *this;
+}
+
+Date& Date::operator-=(Timespan other) {
+ timespan -= other;
+ return *this;
+}
+
+Date& Date::operator++() {
+ timespan++;
+ return *this;
+}
+
+Date Date::operator++(int) {
+ Date old = *this;
+ ++(*this);
+ return old;
+}
+
+Date::operator std::string() const {
+ std::stringstream ss;
+ ss << *this;
+ return ss.str();
+}
+
+std::ostream& OpenVic2::operator<<(std::ostream& out, Date date) {
+ return out << (int) date.getYear() << '.' << (int) date.getMonth() << '.' << (int) date.getDay();
+}
+
+// Parsed from string of the form YYYY.MM.DD
+Date Date::from_string(std::string const& date) {
+ year_t year = 0;
+ month_t month = 1;
+ day_t day = 1;
+
+ size_t first_pos = 0;
+ while (first_pos < date.length() && std::isdigit(date[first_pos++]));
+ year = atoi(date.substr(0, first_pos).c_str());
+ if (first_pos < date.length()) {
+ if (date[first_pos] == '.') {
+ size_t second_pos = first_pos + 1;
+ while (second_pos < date.length() && std::isdigit(date[second_pos++]));
+ month = atoi(date.substr(first_pos, second_pos - first_pos).c_str());
+ if (second_pos < date.length()) {
+ if (date[second_pos] == '.') {
+ size_t third_pos = second_pos + 1;
+ while (third_pos < date.length() && std::isdigit(date[third_pos++]));
+ day = atoi(date.substr(second_pos, third_pos - second_pos).c_str());
+ if (third_pos < date.length())
+ Logger::error("Unexpected string \"", date.substr(third_pos), "\" at the end of date ", date);
+ } else Logger::error("Unexpected character \"", date[second_pos], "\" in date ", date);
+ }
+ } else Logger::error("Unexpected character \"", date[first_pos], "\" in date ", date);
+ }
+ return _dateToTimespan(year, month, day);
+};
diff --git a/extension/src/openvic2/Date.hpp b/extension/src/openvic2/Date.hpp
new file mode 100644
index 0000000..b19602b
--- /dev/null
+++ b/extension/src/openvic2/Date.hpp
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <ostream>
+
+namespace OpenVic2 {
+ // A relative period between points in time, measured in days
+ struct Timespan {
+ using day_t = int64_t;
+ private:
+ day_t days;
+ public:
+ Timespan(day_t value = 0);
+
+ bool operator<(Timespan other) const;
+ bool operator>(Timespan other) const;
+ bool operator<=(Timespan other) const;
+ bool operator>=(Timespan other) const;
+ bool operator==(Timespan other) const;
+ bool operator!=(Timespan other) const;
+
+ Timespan operator+(Timespan other) const;
+ Timespan operator-(Timespan other) const;
+ Timespan operator*(day_t factor) const;
+ Timespan operator/(day_t factor) const;
+ Timespan& operator+=(Timespan other);
+ Timespan& operator-=(Timespan other);
+ Timespan& operator++();
+ Timespan operator++(int);
+
+ explicit operator day_t() const;
+ explicit operator double() const;
+ explicit operator std::string() const;
+ };
+ std::ostream& operator<< (std::ostream& out, Timespan timespan);
+
+ // Represents an in-game date
+ // Note: Current implementation does not account for leap-years, or dates before Year 0
+ struct Date {
+ using year_t = uint16_t;
+ using month_t = uint8_t;
+ using day_t = uint8_t;
+
+ static constexpr Timespan::day_t MONTHS_IN_YEAR = 12;
+ static constexpr Timespan::day_t DAYS_IN_YEAR = 365;
+ static constexpr Timespan::day_t DAYS_IN_MONTH[MONTHS_IN_YEAR] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
+ static constexpr Timespan::day_t DAYS_UP_TO_MONTH[MONTHS_IN_YEAR] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 };
+ private:
+ // Number of days since Jan 1st, Year 0
+ Timespan timespan;
+
+ static Timespan _dateToTimespan(year_t year, month_t month, day_t day);
+ public:
+ // The Timespan is considered to be the number of days since Jan 1st, Year 0
+ Date(Timespan total_days);
+ // Year month day specification
+ Date(year_t year = 0, month_t month = 1, day_t day = 1);
+
+ year_t getYear() const;
+ month_t getMonth() const;
+ day_t getDay() const;
+
+ bool operator<(Date other) const;
+ bool operator>(Date other) const;
+ bool operator<=(Date other) const;
+ bool operator>=(Date other) const;
+ bool operator==(Date other) const;
+ bool operator!=(Date other) const;
+
+ Date operator+(Timespan other) const;
+ Timespan operator-(Date other) const;
+ Date& operator+=(Timespan other);
+ Date& operator-=(Timespan other);
+ Date& operator++();
+ Date operator++(int);
+
+ explicit operator std::string() const;
+ // Parsed from string of the form YYYY.MM.DD
+ static Date from_string(std::string const& date);
+ };
+ std::ostream& operator<< (std::ostream& out, Date date);
+}
diff --git a/extension/src/openvic2/GameAdvancementHook.cpp b/extension/src/openvic2/GameAdvancementHook.cpp
new file mode 100644
index 0000000..4b9bc25
--- /dev/null
+++ b/extension/src/openvic2/GameAdvancementHook.cpp
@@ -0,0 +1,72 @@
+#include "openvic2/GameAdvancementHook.hpp"
+
+using namespace OpenVic2;
+
+const std::vector<std::chrono::milliseconds> GameAdvancementHook::GAME_SPEEDS = {
+ std::chrono::milliseconds{ 4000 },
+ std::chrono::milliseconds{ 3000 },
+ std::chrono::milliseconds{ 2000 },
+ std::chrono::milliseconds{ 1000 },
+ std::chrono::milliseconds{ 100 },
+ std::chrono::milliseconds{ 1 } };
+
+GameAdvancementHook::GameAdvancementHook(AdvancementFunction tickFunction, RefreshFunction updateFunction, bool startPaused, speed_t startingSpeed)
+ : triggerFunction{ tickFunction }, refreshFunction{ updateFunction }, isPaused{ startPaused } {
+ lastPolledTime = std::chrono::high_resolution_clock::now();
+ setSimulationSpeed(startingSpeed);
+}
+
+void GameAdvancementHook::setSimulationSpeed(speed_t speed) {
+ if (speed < 0)
+ currentSpeed = 0;
+ else if (speed >= GAME_SPEEDS.size())
+ currentSpeed = GAME_SPEEDS.size() - 1;
+ else
+ currentSpeed = speed;
+}
+
+GameAdvancementHook::speed_t GameAdvancementHook::getSimulationSpeed() const {
+ return currentSpeed;
+}
+
+void GameAdvancementHook::increaseSimulationSpeed() {
+ setSimulationSpeed(currentSpeed + 1);
+}
+
+void GameAdvancementHook::decreaseSimulationSpeed() {
+ setSimulationSpeed(currentSpeed - 1);
+}
+
+bool GameAdvancementHook::canIncreaseSimulationSpeed() const {
+ return currentSpeed + 1 < GAME_SPEEDS.size();
+}
+
+bool GameAdvancementHook::canDecreaseSimulationSpeed() const {
+ return currentSpeed > 0;
+}
+
+GameAdvancementHook& GameAdvancementHook::operator++() {
+ increaseSimulationSpeed();
+ return *this;
+};
+
+GameAdvancementHook& GameAdvancementHook::operator--() {
+ decreaseSimulationSpeed();
+ return *this;
+};
+
+void GameAdvancementHook::conditionallyAdvanceGame() {
+ if (!isPaused) {
+ std::chrono::time_point<std::chrono::high_resolution_clock> currentTime = std::chrono::high_resolution_clock::now();
+ if (std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastPolledTime) >= GAME_SPEEDS[currentSpeed]) {
+ lastPolledTime = currentTime;
+ if (triggerFunction) triggerFunction();
+ }
+ }
+ if (refreshFunction) refreshFunction();
+}
+
+void GameAdvancementHook::reset() {
+ isPaused = true;
+ currentSpeed = 0;
+}
diff --git a/extension/src/openvic2/GameAdvancementHook.hpp b/extension/src/openvic2/GameAdvancementHook.hpp
new file mode 100644
index 0000000..07f8414
--- /dev/null
+++ b/extension/src/openvic2/GameAdvancementHook.hpp
@@ -0,0 +1,42 @@
+#pragma once
+
+#include <chrono>
+#include <functional>
+#include <vector>
+
+namespace OpenVic2 {
+ //Conditionally advances game with provided behaviour
+ //Class governs game speed and pause state
+ class GameAdvancementHook {
+ public:
+ using AdvancementFunction = std::function<void()>;
+ using RefreshFunction = std::function<void()>;
+ using speed_t = int8_t;
+
+ //Minimum number of miliseconds before the simulation advances
+ static const std::vector<std::chrono::milliseconds> GAME_SPEEDS;
+
+ private:
+ std::chrono::time_point<std::chrono::high_resolution_clock> lastPolledTime;
+ //A function pointer that advances the simulation, intended to be a capturing lambda or something similar. May need to be reworked later
+ AdvancementFunction triggerFunction;
+ RefreshFunction refreshFunction;
+ speed_t currentSpeed;
+
+ public:
+ bool isPaused;
+
+ GameAdvancementHook(AdvancementFunction tickFunction, RefreshFunction updateFunction, bool startPaused = true, speed_t startingSpeed = 0);
+
+ void setSimulationSpeed(speed_t speed);
+ speed_t getSimulationSpeed() const;
+ void increaseSimulationSpeed();
+ void decreaseSimulationSpeed();
+ bool canIncreaseSimulationSpeed() const;
+ bool canDecreaseSimulationSpeed() const;
+ GameAdvancementHook& operator++();
+ GameAdvancementHook& operator--();
+ void conditionallyAdvanceGame();
+ void reset();
+ };
+} \ No newline at end of file
diff --git a/extension/src/openvic2/GameManager.cpp b/extension/src/openvic2/GameManager.cpp
new file mode 100644
index 0000000..78992f1
--- /dev/null
+++ b/extension/src/openvic2/GameManager.cpp
@@ -0,0 +1,46 @@
+#include "openvic2/GameManager.hpp"
+
+#include "openvic2/Logger.hpp"
+
+using namespace OpenVic2;
+
+GameManager::GameManager(state_updated_func_t state_updated_callback)
+ : clock{ [this]() { tick(); }, [this]() { update_state(); } }, state_updated{ state_updated_callback } {}
+
+void GameManager::set_needs_update() {
+ needs_update = true;
+}
+
+void GameManager::update_state() {
+ if (needs_update) {
+ Logger::info("Update: ", today);
+ map.update_state(today);
+ if (state_updated) state_updated();
+ needs_update = false;
+ }
+}
+
+void GameManager::tick() {
+ today++;
+ Logger::info("Tick: ", today);
+ map.tick(today);
+ set_needs_update();
+}
+
+return_t GameManager::setup() {
+ clock.reset();
+ today = { 1836 };
+ set_needs_update();
+ return map.generate_province_buildings(building_manager);
+}
+
+Date const& GameManager::get_today() const {
+ return today;
+}
+
+return_t GameManager::expand_building(Province::index_t province_index, std::string const& building_type_identifier) {
+ set_needs_update();
+ Province* province = map.get_province_by_index(province_index);
+ if (province == nullptr) return FAILURE;
+ return province->expand_building(building_type_identifier);
+}
diff --git a/extension/src/openvic2/GameManager.hpp b/extension/src/openvic2/GameManager.hpp
new file mode 100644
index 0000000..65cd566
--- /dev/null
+++ b/extension/src/openvic2/GameManager.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "openvic2/GameAdvancementHook.hpp"
+#include "openvic2/map/Map.hpp"
+
+namespace OpenVic2 {
+ struct GameManager {
+ using state_updated_func_t = std::function<void()>;
+
+ Map map;
+ BuildingManager building_manager;
+ GameAdvancementHook clock;
+ private:
+ Date today;
+ state_updated_func_t state_updated;
+ bool needs_update;
+
+ void set_needs_update();
+ void update_state();
+ void tick();
+ public:
+ GameManager(state_updated_func_t state_updated_callback);
+
+ return_t setup();
+
+ Date const& get_today() const;
+ return_t expand_building(Province::index_t province_index, std::string const& building_type_identifier);
+ };
+}
diff --git a/extension/src/openvic2/Logger.cpp b/extension/src/openvic2/Logger.cpp
new file mode 100644
index 0000000..56d74ab
--- /dev/null
+++ b/extension/src/openvic2/Logger.cpp
@@ -0,0 +1,26 @@
+#include "openvic2/Logger.hpp"
+
+#include <iostream>
+
+using namespace OpenVic2;
+
+Logger::log_func_t Logger::info_func = [](std::string&& str) { std::cout << str; };
+Logger::log_func_t Logger::error_func = [](std::string&& str) { std::cerr << str; };
+
+char const* Logger::get_filename(char const* filepath) {
+ if (filepath == nullptr) return nullptr;
+ char const* last_slash = filepath;
+ while (*filepath != '\0') {
+ if (*filepath == '\\' || *filepath == '/') last_slash = filepath + 1;
+ filepath++;
+ }
+ return last_slash;
+}
+
+void Logger::set_info_func(log_func_t log_func) {
+ info_func = log_func;
+}
+
+void Logger::set_error_func(log_func_t log_func) {
+ error_func = log_func;
+}
diff --git a/extension/src/openvic2/Logger.hpp b/extension/src/openvic2/Logger.hpp
new file mode 100644
index 0000000..624df29
--- /dev/null
+++ b/extension/src/openvic2/Logger.hpp
@@ -0,0 +1,83 @@
+#pragma once
+
+#include <functional>
+#include <sstream>
+#ifdef __cpp_lib_source_location
+#include <source_location>
+#endif
+
+namespace OpenVic2 {
+
+ #ifndef __cpp_lib_source_location
+ #include <string>
+ //Implementation of std::source_location for compilers that do not support it
+ //Note: uses non-standard extensions that are supported by Clang, GCC, and MSVC
+ //https://clang.llvm.org/docs/LanguageExtensions.html#source-location-builtins
+ //https://stackoverflow.com/a/67970107
+ class source_location {
+ std::string _file;
+ int _line;
+ std::string _function;
+
+ public:
+ source_location(std::string f, int l, std::string n) : _file(f), _line(l), _function(n) {}
+ static source_location current(std::string f = __builtin_FILE(), int l = __builtin_LINE(), std::string n = __builtin_FUNCTION()) {
+ return source_location(f, l, n);
+ }
+
+ inline char const* file_name() const { return _file.c_str(); }
+ inline int line() const {return _line; }
+ inline char const* function_name() const { return _function.c_str(); }
+ };
+ #endif
+
+ class Logger {
+ using log_func_t = std::function<void(std::string&&)>;
+
+ #ifdef __cpp_lib_source_location
+ using source_location = std::source_location;
+ #else
+ using source_location = OpenVic2::source_location;
+ #endif
+
+ static log_func_t info_func, error_func;
+
+ static char const* get_filename(char const* filepath);
+
+ template <typename... Ts>
+ struct log {
+ log(log_func_t log_func, Ts&&... ts, const source_location& location) {
+ if (log_func) {
+ std::stringstream stream;
+ stream << std::endl << get_filename(location.file_name()) << "(" << location.line() << ") `" << location.function_name() << "`: ";
+ ((stream << std::forward<Ts>(ts)), ...);
+ stream << std::endl;
+ log_func(stream.str());
+ }
+ }
+ };
+ public:
+ static void set_info_func(log_func_t log_func);
+ static void set_error_func(log_func_t log_func);
+
+ template <typename... Ts>
+ struct info {
+ info(Ts&&... ts, const source_location& location = source_location::current()) {
+ log<Ts...>{ info_func, std::forward<Ts>(ts)..., location };
+ }
+ };
+
+ template <typename... Ts>
+ info(Ts&&...) -> info<Ts...>;
+
+ template <typename... Ts>
+ struct error {
+ error(Ts&&... ts, const source_location& location = source_location::current()) {
+ log<Ts...>{ error_func, std::forward<Ts>(ts)..., location };
+ }
+ };
+
+ template <typename... Ts>
+ error(Ts&&...) -> error<Ts...>;
+ };
+}
diff --git a/extension/src/openvic2/Types.cpp b/extension/src/openvic2/Types.cpp
new file mode 100644
index 0000000..861ab50
--- /dev/null
+++ b/extension/src/openvic2/Types.cpp
@@ -0,0 +1,13 @@
+#include "openvic2/Types.hpp"
+
+#include <cassert>
+
+using namespace OpenVic2;
+
+HasIdentifier::HasIdentifier(std::string const& new_identifier) : identifier{ new_identifier } {
+ assert(!identifier.empty());
+}
+
+std::string const& HasIdentifier::get_identifier() const {
+ return identifier;
+}
diff --git a/extension/src/openvic2/Types.hpp b/extension/src/openvic2/Types.hpp
new file mode 100644
index 0000000..98e92ce
--- /dev/null
+++ b/extension/src/openvic2/Types.hpp
@@ -0,0 +1,100 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "openvic2/Logger.hpp"
+
+namespace OpenVic2 {
+ using return_t = bool;
+ // This mirrors godot::Error, where `OK = 0` and `FAILED = 1`.
+ static constexpr return_t SUCCESS = false, FAILURE = true;
+
+ /*
+ * Base class for objects with a non-empty string identifier,
+ * uniquely named instances of which can be entered into an
+ * IdentifierRegistry instance.
+ */
+ class HasIdentifier {
+ const std::string identifier;
+ protected:
+ HasIdentifier(std::string const& new_identifier);
+ public:
+ HasIdentifier(HasIdentifier const&) = delete;
+ HasIdentifier(HasIdentifier&&) = default;
+ HasIdentifier& operator=(HasIdentifier const&) = delete;
+ HasIdentifier& operator=(HasIdentifier&&) = delete;
+
+ std::string const& get_identifier() const;
+ };
+
+ /*
+ * Template for a list of objects with unique string identifiers that can
+ * be locked to prevent any further additions. The template argument T is
+ * the type of object that the registry will store, and the second part ensures
+ * that HasIdentifier is a base class of T.
+ */
+ template<class T, typename std::enable_if<std::is_base_of<HasIdentifier, T>::value>::type* = nullptr>
+ class IdentifierRegistry {
+ const std::string name;
+ std::vector<T> items;
+ bool locked = false;
+ public:
+ IdentifierRegistry(std::string const& new_name) : name(new_name) {}
+ return_t add_item(T&& item) {
+ if (locked) {
+ Logger::error("Cannot add item to the ", name, " registry - locked!");
+ return FAILURE;
+ }
+ T const* old_item = get_item_by_identifier(item.get_identifier());
+ if (old_item != nullptr) {
+ Logger::error("Cannot add item to the ", name, " registry - an item with the identifier \"", item.get_identifier(), "\" already exists!");
+ return FAILURE;
+ }
+ items.push_back(std::move(item));
+ return SUCCESS;
+ }
+ void lock(bool log = true) {
+ if (locked) {
+ Logger::error("Failed to lock ", name, " registry - already locked!");
+ } else {
+ locked = true;
+ if (log) Logger::info("Locked ", name, " registry after registering ", get_item_count(), " items");
+ }
+ }
+ bool is_locked() const {
+ return locked;
+ }
+ void reset() {
+ items.clear();
+ locked = false;
+ }
+ size_t get_item_count() const {
+ return items.size();
+ }
+ T* get_item_by_identifier(std::string const& identifier) {
+ if (!identifier.empty())
+ for (T& item : items)
+ if (item.get_identifier() == identifier) return &item;
+ return nullptr;
+ }
+ T const* get_item_by_identifier(std::string const& identifier) const {
+ if (!identifier.empty())
+ for (T const& item : items)
+ if (item.get_identifier() == identifier) return &item;
+ return nullptr;
+ }
+ T* get_item_by_index(size_t index) {
+ return index < items.size() ? &items[index] : nullptr;
+ }
+ T const* get_item_by_index(size_t index) const {
+ return index < items.size() ? &items[index] : nullptr;
+ }
+ std::vector<T>& get_items() {
+ return items;
+ }
+ std::vector<T> const& get_items() const {
+ return items;
+ }
+ };
+}
diff --git a/extension/src/openvic2/map/Building.cpp b/extension/src/openvic2/map/Building.cpp
new file mode 100644
index 0000000..00e121b
--- /dev/null
+++ b/extension/src/openvic2/map/Building.cpp
@@ -0,0 +1,124 @@
+#include "openvic2/map/Building.hpp"
+
+#include <cassert>
+
+#include "openvic2/Logger.hpp"
+#include "openvic2/map/Province.hpp"
+
+using namespace OpenVic2;
+
+Building::Building(BuildingType const& new_type) : HasIdentifier{ new_type.get_identifier() }, type{ new_type } {}
+
+bool Building::_can_expand() const {
+ return level < type.get_max_level();
+}
+
+BuildingType const& Building::get_type() const {
+ return type;
+}
+
+Building::level_t Building::get_level() const {
+ return level;
+}
+
+Building::ExpansionState Building::get_expansion_state() const {
+ return expansion_state;
+}
+
+Date const& Building::get_start_date() const {
+ return start;
+}
+
+Date const& Building::get_end_date() const {
+ return end;
+}
+
+float Building::get_expansion_progress() const {
+ return expansion_progress;
+}
+
+return_t Building::expand() {
+ if (expansion_state == ExpansionState::CanExpand) {
+ expansion_state = ExpansionState::Preparing;
+ expansion_progress = 0.0f;
+ return SUCCESS;
+ }
+ return FAILURE;
+}
+
+/* REQUIREMENTS:
+ * MAP-71, MAP-74, MAP-77
+ */
+void Building::update_state(Date const& today) {
+ switch (expansion_state) {
+ case ExpansionState::Preparing:
+ start = today;
+ end = start + type.get_build_time();
+ break;
+ case ExpansionState::Expanding:
+ expansion_progress = static_cast<double>(today - start) / static_cast<double>(end - start);
+ break;
+ default: expansion_state = _can_expand() ? ExpansionState::CanExpand : ExpansionState::CannotExpand;
+ }
+}
+
+void Building::tick(Date const& today) {
+ if (expansion_state == ExpansionState::Preparing) {
+ expansion_state = ExpansionState::Expanding;
+ }
+ if (expansion_state == ExpansionState::Expanding) {
+ if (end <= today) {
+ level++;
+ expansion_state = ExpansionState::CannotExpand;
+ }
+ }
+}
+
+BuildingType::BuildingType(std::string const& new_identifier, Building::level_t new_max_level, Timespan new_build_time) :
+ HasIdentifier{ new_identifier }, max_level{ new_max_level }, build_time{ new_build_time } {
+ assert(new_max_level >= 0);
+ assert(build_time >= 0);
+}
+
+Building::level_t BuildingType::get_max_level() const {
+ return max_level;
+}
+
+Timespan BuildingType::get_build_time() const {
+ return build_time;
+}
+
+BuildingManager::BuildingManager() : building_types{ "building types" } {}
+
+return_t BuildingManager::add_building_type(std::string const& identifier, Building::level_t max_level, Timespan build_time) {
+ if (identifier.empty()) {
+ Logger::error("Invalid building type identifier - empty!");
+ return FAILURE;
+ }
+ if (max_level < 0) {
+ Logger::error("Invalid building type max level: ", max_level);
+ return FAILURE;
+ }
+ if (build_time < 0) {
+ Logger::error("Invalid building type build time: ", build_time);
+ return FAILURE;
+ }
+ return building_types.add_item({ identifier, max_level, build_time });
+}
+
+void BuildingManager::lock_building_types() {
+ building_types.lock();
+}
+
+BuildingType const* BuildingManager::get_building_type_by_identifier(std::string const& identifier) const {
+ return building_types.get_item_by_identifier(identifier);
+}
+
+return_t BuildingManager::generate_province_buildings(Province& province) const {
+ return_t ret = SUCCESS;
+ province.reset_buildings();
+ for (BuildingType const& type : building_types.get_items())
+ if (province.add_building(type) != SUCCESS) ret = FAILURE;
+ province.lock_buildings();
+ return ret;
+}
diff --git a/extension/src/openvic2/map/Building.hpp b/extension/src/openvic2/map/Building.hpp
new file mode 100644
index 0000000..1305014
--- /dev/null
+++ b/extension/src/openvic2/map/Building.hpp
@@ -0,0 +1,75 @@
+#pragma once
+
+#include <vector>
+
+#include "openvic2/Types.hpp"
+#include "openvic2/Date.hpp"
+
+namespace OpenVic2 {
+ struct Province;
+ struct BuildingType;
+
+ /* REQUIREMENTS:
+ * MAP-11, MAP-72, MAP-73
+ * MAP-12, MAP-75, MAP-76
+ * MAP-13, MAP-78, MAP-79
+ */
+ struct Building : HasIdentifier {
+ friend struct Province;
+
+ using level_t = int8_t;
+
+ enum class ExpansionState { CannotExpand, CanExpand, Preparing, Expanding };
+ private:
+ BuildingType const& type;
+ level_t level = 0;
+ ExpansionState expansion_state = ExpansionState::CannotExpand;
+ Date start, end;
+ float expansion_progress;
+
+ Building(BuildingType const& new_type);
+
+ bool _can_expand() const;
+ public:
+ Building(Building&&) = default;
+
+ BuildingType const& get_type() const;
+ level_t get_level() const;
+ ExpansionState get_expansion_state() const;
+ Date const& get_start_date() const;
+ Date const& get_end_date() const;
+ float get_expansion_progress() const;
+
+ return_t expand();
+ void update_state(Date const& today);
+ void tick(Date const& today);
+ };
+
+ struct BuildingManager;
+
+ struct BuildingType : HasIdentifier {
+ friend struct BuildingManager;
+ private:
+ const Building::level_t max_level;
+ const Timespan build_time;
+
+ BuildingType(std::string const& new_identifier, Building::level_t new_max_level, Timespan new_build_time);
+ public:
+ BuildingType(BuildingType&&) = default;
+
+ Building::level_t get_max_level() const;
+ Timespan get_build_time() const;
+ };
+
+ struct BuildingManager {
+ private:
+ IdentifierRegistry<BuildingType> building_types;
+ public:
+ BuildingManager();
+
+ return_t add_building_type(std::string const& identifier, Building::level_t max_level, Timespan build_time);
+ void lock_building_types();
+ BuildingType const* get_building_type_by_identifier(std::string const& identifier) const;
+ return_t generate_province_buildings(Province& province) const;
+ };
+}
diff --git a/extension/src/openvic2/map/Map.cpp b/extension/src/openvic2/map/Map.cpp
new file mode 100644
index 0000000..b5cf144
--- /dev/null
+++ b/extension/src/openvic2/map/Map.cpp
@@ -0,0 +1,320 @@
+#include "openvic2/map/Map.hpp"
+
+#include <cassert>
+#include <unordered_set>
+
+#include "openvic2/Logger.hpp"
+
+using namespace OpenVic2;
+
+Mapmode::Mapmode(index_t new_index, std::string const& new_identifier, colour_func_t new_colour_func)
+ : HasIdentifier{ new_identifier }, index{ new_index }, colour_func{ new_colour_func } {
+ assert(colour_func != nullptr);
+}
+
+Mapmode::index_t Mapmode::get_index() const {
+ return index;
+}
+
+Province::colour_t Mapmode::get_colour(Map const& map, Province const& province) const {
+ return colour_func ? colour_func(map, province) : Province::NULL_COLOUR;
+}
+
+Map::Map() : provinces{ "provinces" }, regions{ "regions" }, mapmodes{ "mapmodes" } {}
+
+return_t Map::add_province(std::string const& identifier, Province::colour_t colour) {
+ if (provinces.get_item_count() >= Province::MAX_INDEX) {
+ Logger::error("The map's province list is full - there can be at most ", Province::MAX_INDEX, " provinces");
+ return FAILURE;
+ }
+ if (identifier.empty()) {
+ Logger::error("Invalid province identifier - empty!");
+ return FAILURE;
+ }
+ if (colour == Province::NULL_COLOUR || colour > Province::MAX_COLOUR) {
+ Logger::error("Invalid province colour: ", Province::colour_to_hex_string(colour));
+ return FAILURE;
+ }
+ Province new_province{ static_cast<Province::index_t>(provinces.get_item_count() + 1), identifier, colour };
+ Province const* old_province = get_province_by_colour(colour);
+ if (old_province != nullptr) {
+ Logger::error("Duplicate province colours: ", old_province->to_string(), " and ", new_province.to_string());
+ return FAILURE;
+ }
+ return provinces.add_item(std::move(new_province));
+}
+
+void Map::lock_provinces() {
+ provinces.lock();
+}
+
+return_t Map::set_water_province(std::string const& identifier) {
+ if (water_provinces_locked) {
+ Logger::error("The map's water provinces have already been locked!");
+ return FAILURE;
+ }
+ Province* province = get_province_by_identifier(identifier);
+ if (province == nullptr) {
+ Logger::error("Unrecognised water province identifier: ", identifier);
+ return FAILURE;
+ }
+ if (province->is_water()) {
+ Logger::error("Province ", identifier, " is already a water province!");
+ return FAILURE;
+ }
+ province->water = true;
+ water_province_count++;
+ return SUCCESS;
+}
+
+void Map::lock_water_provinces() {
+ water_provinces_locked = true;
+ Logger::info("Locked water provinces after registering ", water_province_count);
+}
+
+return_t Map::add_region(std::string const& identifier, std::vector<std::string> const& province_identifiers) {
+ if (identifier.empty()) {
+ Logger::error("Invalid region identifier - empty!");
+ return FAILURE;
+ }
+ Region new_region{ identifier };
+ return_t ret = SUCCESS;
+ for (std::string const& province_identifier : province_identifiers) {
+ Province* province = get_province_by_identifier(province_identifier);
+ if (province != nullptr) {
+ if (new_region.contains_province(province)) {
+ Logger::error("Duplicate province identifier ", province_identifier);
+ ret = FAILURE;
+ } else {
+ size_t other_region_index = reinterpret_cast<size_t>(province->get_region());
+ if (other_region_index != 0) {
+ other_region_index--;
+ if (other_region_index < regions.get_item_count())
+ Logger::error("Province ", province_identifier, " is already part of ", regions.get_item_by_index(other_region_index)->get_identifier());
+ else
+ Logger::error("Province ", province_identifier, " is already part of an unknown region with index ", other_region_index);
+ ret = FAILURE;
+ } else new_region.provinces.insert(province);
+ }
+ } else {
+ Logger::error("Invalid province identifier ", province_identifier);
+ ret = FAILURE;
+ }
+ }
+ if (!new_region.get_province_count()) {
+ Logger::error("No valid provinces in region's list");
+ return FAILURE;
+ }
+
+ // Used to detect provinces listed in multiple regions, will
+ // be corrected once regions is stable (i.e. lock_regions).
+ Region* tmp_region_index = reinterpret_cast<Region*>(regions.get_item_count());
+ for (Province* province : new_region.get_provinces())
+ province->region = tmp_region_index;
+ if (regions.add_item(std::move(new_region)) != SUCCESS) ret = FAILURE;
+ return ret;
+}
+
+void Map::lock_regions() {
+ regions.lock();
+ for (Region& region : regions.get_items())
+ for (Province* province : region.get_provinces())
+ province->region = &region;
+}
+
+size_t Map::get_province_count() const {
+ return provinces.get_item_count();
+}
+
+Province* Map::get_province_by_index(Province::index_t index) {
+ return index != Province::NULL_INDEX ? provinces.get_item_by_index(index - 1) : nullptr;
+}
+
+Province const* Map::get_province_by_index(Province::index_t index) const {
+ return index != Province::NULL_INDEX ? provinces.get_item_by_index(index - 1) : nullptr;
+}
+
+Province* Map::get_province_by_identifier(std::string const& identifier) {
+ return provinces.get_item_by_identifier(identifier);
+}
+
+Province const* Map::get_province_by_identifier(std::string const& identifier) const {
+ return provinces.get_item_by_identifier(identifier);
+}
+
+Province* Map::get_province_by_colour(Province::colour_t colour) {
+ if (colour != Province::NULL_COLOUR)
+ for (Province& province : provinces.get_items())
+ if (province.get_colour() == colour) return &province;
+ return nullptr;
+}
+
+Province const* Map::get_province_by_colour(Province::colour_t colour) const {
+ if (colour != Province::NULL_COLOUR)
+ for (Province const& province : provinces.get_items())
+ if (province.get_colour() == colour) return &province;
+ return nullptr;
+}
+
+Province::index_t Map::get_province_index_at(size_t x, size_t y) const {
+ if (x < width && y < height) return province_index_image[x + y * width];
+ return Province::NULL_INDEX;
+}
+
+Region* Map::get_region_by_identifier(std::string const& identifier) {
+ return regions.get_item_by_identifier(identifier);
+}
+
+Region const* Map::get_region_by_identifier(std::string const& identifier) const {
+ return regions.get_item_by_identifier(identifier);
+}
+
+static Province::colour_t colour_at(uint8_t const* colour_data, int32_t idx) {
+ return (colour_data[idx * 3] << 16) | (colour_data[idx * 3 + 1] << 8) | colour_data[idx * 3 + 2];
+}
+
+return_t Map::generate_province_index_image(size_t new_width, size_t new_height, uint8_t const* colour_data) {
+ if (!province_index_image.empty()) {
+ Logger::error("Province index image has already been generated!");
+ return FAILURE;
+ }
+ if (!provinces.is_locked()) {
+ Logger::error("Province index image cannot be generated until after provinces are locked!");
+ return FAILURE;
+ }
+ if (new_width < 1 || new_height < 1) {
+ Logger::error("Invalid province image dimensions: ", new_width, "x", new_height);
+ return FAILURE;
+ }
+ if (colour_data == nullptr) {
+ Logger::error("Province colour data pointer is null!");
+ return FAILURE;
+ }
+ width = new_width;
+ height = new_height;
+ province_index_image.resize(width * height);
+
+ std::vector<bool> province_checklist(provinces.get_item_count());
+ return_t ret = SUCCESS;
+ std::unordered_set<Province::colour_t> unrecognised_colours;
+
+ for (int32_t y = 0; y < height; ++y) {
+ for (int32_t x = 0; x < width; ++x) {
+ const int32_t idx = x + y * width;
+ const Province::colour_t colour = colour_at(colour_data, idx);
+ if (x > 0) {
+ const int32_t jdx = idx - 1;
+ if (colour_at(colour_data, jdx) == colour) {
+ province_index_image[idx] = province_index_image[jdx];
+ continue;
+ }
+ }
+ if (y > 0) {
+ const int32_t jdx = idx - width;
+ if (colour_at(colour_data, jdx) == colour) {
+ province_index_image[idx] = province_index_image[jdx];
+ continue;
+ }
+ }
+ Province const* province = get_province_by_colour(colour);
+ if (province != nullptr) {
+ const Province::index_t index = province->get_index();
+ province_index_image[idx] = index;
+ province_checklist[index - 1] = true;
+ continue;
+ }
+ if (unrecognised_colours.find(colour) == unrecognised_colours.end()) {
+ unrecognised_colours.insert(colour);
+ Logger::error("Unrecognised province colour ", Province::colour_to_hex_string(colour), " at (", x, ", ", y, ")");
+ ret = FAILURE;
+ }
+ province_index_image[idx] = Province::NULL_INDEX;
+ }
+ }
+
+ for (size_t idx = 0; idx < province_checklist.size(); ++idx) {
+ if (!province_checklist[idx]) {
+ Logger::error("Province missing from shape image: ", provinces.get_item_by_index(idx)->to_string());
+ ret = FAILURE;
+ }
+ }
+ return ret;
+}
+
+size_t Map::get_width() const {
+ return width;
+}
+
+size_t Map::get_height() const {
+ return height;
+}
+
+std::vector<Province::index_t> const& Map::get_province_index_image() const {
+ return province_index_image;
+}
+
+return_t Map::add_mapmode(std::string const& identifier, Mapmode::colour_func_t colour_func) {
+ if (identifier.empty()) {
+ Logger::error("Invalid mapmode identifier - empty!");
+ return FAILURE;
+ }
+ if (colour_func == nullptr) {
+ Logger::error("Mapmode colour function is null for identifier: ", identifier);
+ return FAILURE;
+ }
+ return mapmodes.add_item({ mapmodes.get_item_count(), identifier, colour_func });
+}
+
+void Map::lock_mapmodes() {
+ mapmodes.lock();
+}
+
+size_t Map::get_mapmode_count() const {
+ return mapmodes.get_item_count();
+}
+
+Mapmode const* Map::get_mapmode_by_index(size_t index) const {
+ return mapmodes.get_item_by_index(index);
+}
+
+Mapmode const* Map::get_mapmode_by_identifier(std::string const& identifier) const {
+ return mapmodes.get_item_by_identifier(identifier);
+}
+
+return_t Map::generate_mapmode_colours(Mapmode::index_t index, uint8_t* target) const {
+ if (target == nullptr) {
+ Logger::error("Mapmode colour target pointer is null!");
+ return FAILURE;
+ }
+ Mapmode const* mapmode = mapmodes.get_item_by_index(index);
+ if (mapmode == nullptr) {
+ Logger::error("Invalid mapmode index: ", index);
+ return FAILURE;
+ }
+ target += 4; // Skip past Province::NULL_INDEX
+ for (Province const& province : provinces.get_items()) {
+ const Province::colour_t colour = mapmode->get_colour(*this, province);
+ *target++ = (colour >> 16) & 0xFF;
+ *target++ = (colour >> 8) & 0xFF;
+ *target++ = colour & 0xFF;
+ *target++ = province.is_water() ? 0 : 255;
+ }
+ return SUCCESS;
+}
+
+return_t Map::generate_province_buildings(BuildingManager const& manager) {
+ return_t ret = SUCCESS;
+ for (Province& province : provinces.get_items())
+ if (manager.generate_province_buildings(province) != SUCCESS) ret = FAILURE;
+ return ret;
+}
+
+void Map::update_state(Date const& today) {
+ for (Province& province : provinces.get_items())
+ province.update_state(today);
+}
+
+void Map::tick(Date const& today) {
+ for (Province& province : provinces.get_items())
+ province.tick(today);
+}
diff --git a/extension/src/openvic2/map/Map.hpp b/extension/src/openvic2/map/Map.hpp
new file mode 100644
index 0000000..ebc23be
--- /dev/null
+++ b/extension/src/openvic2/map/Map.hpp
@@ -0,0 +1,76 @@
+#pragma once
+
+#include <functional>
+
+#include "openvic2/map/Region.hpp"
+
+namespace OpenVic2 {
+
+ struct Mapmode : HasIdentifier {
+ friend struct Map;
+
+ using colour_func_t = std::function<Province::colour_t (Map const&, Province const&)>;
+ using index_t = size_t;
+ private:
+ const index_t index;
+ const colour_func_t colour_func;
+
+ Mapmode(index_t new_index, std::string const& new_identifier, colour_func_t new_colour_func);
+ public:
+ index_t get_index() const;
+ Province::colour_t get_colour(Map const& map, Province const& province) const;
+ };
+
+ /* REQUIREMENTS:
+ * MAP-4
+ */
+ struct Map {
+ private:
+ IdentifierRegistry<Province> provinces;
+ IdentifierRegistry<Region> regions;
+ IdentifierRegistry<Mapmode> mapmodes;
+ bool water_provinces_locked = false;
+ size_t water_province_count = 0;
+
+ size_t width = 0, height = 0;
+ std::vector<Province::index_t> province_index_image;
+ public:
+ Map();
+
+ return_t add_province(std::string const& identifier, Province::colour_t colour);
+ void lock_provinces();
+ return_t set_water_province(std::string const& identifier);
+ void lock_water_provinces();
+ return_t add_region(std::string const& identifier, std::vector<std::string> const& province_identifiers);
+ void lock_regions();
+
+ size_t get_province_count() const;
+ Province* get_province_by_index(Province::index_t index);
+ Province const* get_province_by_index(Province::index_t index) const;
+ Province* get_province_by_identifier(std::string const& identifier);
+ Province const* get_province_by_identifier(std::string const& identifier) const;
+ Province* get_province_by_colour(Province::colour_t colour);
+ Province const* get_province_by_colour(Province::colour_t colour) const;
+ Province::index_t get_province_index_at(size_t x, size_t y) const;
+
+ Region* get_region_by_identifier(std::string const& identifier);
+ Region const* get_region_by_identifier(std::string const& identifier) const;
+
+ return_t generate_province_index_image(size_t new_width, size_t new_height, uint8_t const* colour_data);
+ size_t get_width() const;
+ size_t get_height() const;
+ std::vector<Province::index_t> const& get_province_index_image() const;
+
+ return_t add_mapmode(std::string const& identifier, Mapmode::colour_func_t colour_func);
+ void lock_mapmodes();
+ size_t get_mapmode_count() const;
+ Mapmode const* get_mapmode_by_index(Mapmode::index_t index) const;
+ Mapmode const* get_mapmode_by_identifier(std::string const& identifier) const;
+ return_t generate_mapmode_colours(Mapmode::index_t index, uint8_t* target) const;
+
+ return_t generate_province_buildings(BuildingManager const& manager);
+
+ void update_state(Date const& today);
+ void tick(Date const& today);
+ };
+}
diff --git a/extension/src/openvic2/map/Province.cpp b/extension/src/openvic2/map/Province.cpp
new file mode 100644
index 0000000..4360bce
--- /dev/null
+++ b/extension/src/openvic2/map/Province.cpp
@@ -0,0 +1,78 @@
+#include "openvic2/map/Province.hpp"
+
+#include <cassert>
+#include <sstream>
+#include <iomanip>
+
+using namespace OpenVic2;
+
+Province::Province(index_t new_index, std::string const& new_identifier, colour_t new_colour) :
+ HasIdentifier{ new_identifier }, index{ new_index }, colour{ new_colour }, buildings{ "buildings" } {
+ assert(index != NULL_INDEX);
+ assert(colour != NULL_COLOUR);
+}
+
+std::string Province::colour_to_hex_string(colour_t colour) {
+ std::ostringstream stream;
+ stream << std::hex << std::setfill('0') << std::setw(6) << colour;
+ return stream.str();
+}
+
+Province::index_t Province::get_index() const {
+ return index;
+}
+
+Province::colour_t Province::get_colour() const {
+ return colour;
+}
+
+Region* Province::get_region() const {
+ return region;
+}
+
+bool Province::is_water() const {
+ return water;
+}
+
+Province::life_rating_t Province::get_life_rating() const {
+ return life_rating;
+}
+
+return_t Province::add_building(BuildingType const& type) {
+ return buildings.add_item({ type });
+}
+
+void Province::lock_buildings() {
+ buildings.lock(false);
+}
+
+void Province::reset_buildings() {
+ buildings.reset();
+}
+
+std::vector<Building> const& Province::get_buildings() const {
+ return buildings.get_items();
+}
+
+return_t Province::expand_building(std::string const& building_type_identifier) {
+ Building* building = buildings.get_item_by_identifier(building_type_identifier);
+ if (building == nullptr) return FAILURE;
+ return building->expand();
+}
+
+std::string Province::to_string() const {
+ std::stringstream stream;
+ stream << "(#" << std::to_string(index) << ", " << get_identifier() << ", 0x" << colour_to_hex_string(colour) << ")";
+ return stream.str();
+}
+
+void Province::update_state(Date const& today) {
+ for (Building& building : buildings.get_items())
+ building.update_state(today);
+
+}
+
+void Province::tick(Date const& today) {
+ for (Building& building : buildings.get_items())
+ building.tick(today);
+}
diff --git a/extension/src/openvic2/map/Province.hpp b/extension/src/openvic2/map/Province.hpp
new file mode 100644
index 0000000..aa0329c
--- /dev/null
+++ b/extension/src/openvic2/map/Province.hpp
@@ -0,0 +1,50 @@
+#pragma once
+
+#include "openvic2/map/Building.hpp"
+
+namespace OpenVic2 {
+ struct Map;
+ struct Region;
+
+ /* REQUIREMENTS:
+ * MAP-5, MAP-8, MAP-43, MAP-47
+ */
+ struct Province : HasIdentifier {
+ friend struct Map;
+
+ using colour_t = uint32_t;
+ using index_t = uint16_t;
+ using life_rating_t = int8_t;
+
+ static constexpr colour_t NULL_COLOUR = 0, MAX_COLOUR = 0xFFFFFF;
+ static constexpr index_t NULL_INDEX = 0, MAX_INDEX = 0xFFFF;
+ private:
+ const index_t index;
+ const colour_t colour;
+ Region* region = nullptr;
+ bool water = false;
+ life_rating_t life_rating = 0;
+ IdentifierRegistry<Building> buildings;
+
+ Province(index_t new_index, std::string const& new_identifier, colour_t new_colour);
+ public:
+ static std::string colour_to_hex_string(colour_t colour);
+
+ Province(Province&&) = default;
+
+ index_t get_index() const;
+ colour_t get_colour() const;
+ Region* get_region() const;
+ bool is_water() const;
+ life_rating_t get_life_rating() const;
+ return_t add_building(BuildingType const& type);
+ void lock_buildings();
+ void reset_buildings();
+ std::vector<Building> const& get_buildings() const;
+ return_t expand_building(std::string const& building_type_identifier);
+ std::string to_string() const;
+
+ void update_state(Date const& today);
+ void tick(Date const& today);
+ };
+}
diff --git a/extension/src/openvic2/map/Region.cpp b/extension/src/openvic2/map/Region.cpp
new file mode 100644
index 0000000..3e5bee7
--- /dev/null
+++ b/extension/src/openvic2/map/Region.cpp
@@ -0,0 +1,26 @@
+#include "openvic2/map/Region.hpp"
+
+#include <cassert>
+#include <algorithm>
+
+using namespace OpenVic2;
+
+size_t ProvinceSet::get_province_count() const {
+ return provinces.size();
+}
+
+bool ProvinceSet::contains_province(Province const* province) const {
+ return province && std::find(provinces.begin(), provinces.end(), province) != provinces.end();
+}
+
+std::set<Province*> const& ProvinceSet::get_provinces() const {
+ return provinces;
+}
+
+Region::Region(std::string const& new_identifier) : HasIdentifier{ new_identifier } {}
+
+Province::colour_t Region::get_colour() const {
+ if (provinces.empty()) return 0xFF0000;
+ Province const* province = *provinces.cbegin();
+ return province->get_colour();
+}
diff --git a/extension/src/openvic2/map/Region.hpp b/extension/src/openvic2/map/Region.hpp
new file mode 100644
index 0000000..04564fc
--- /dev/null
+++ b/extension/src/openvic2/map/Region.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <set>
+
+#include "openvic2/map/Province.hpp"
+
+namespace OpenVic2 {
+
+ struct ProvinceSet {
+ protected:
+ std::set<Province*> provinces;
+ public:
+ size_t get_province_count() const;
+ bool contains_province(Province const* province) const;
+ std::set<Province*> const& get_provinces() const;
+ };
+
+ /* REQUIREMENTS:
+ * MAP-6, MAP-44, MAP-48
+ */
+ struct Region : HasIdentifier, ProvinceSet {
+ friend struct Map;
+ private:
+ Region(std::string const& new_identifier);
+ public:
+ Region(Region&&) = default;
+
+ Province::colour_t get_colour() const;
+ };
+}
diff --git a/extension/src/register_types.cpp b/extension/src/register_types.cpp
index d1613a5..b99f1a8 100644
--- a/extension/src/register_types.cpp
+++ b/extension/src/register_types.cpp
@@ -1,37 +1,24 @@
#include "register_types.h"
-#include <gdextension_interface.h>
-#include <godot_cpp/core/class_db.hpp>
-#include <godot_cpp/core/defs.hpp>
-#include <godot_cpp/godot.hpp>
+
#include <godot_cpp/classes/engine.hpp>
-#include "TestSingleton.hpp"
-#include "Simulation.hpp"
#include "Checksum.hpp"
#include "LoadLocalisation.hpp"
+#include "GameSingleton.hpp"
+#include "MapMesh.hpp"
using namespace godot;
using namespace OpenVic2;
-static TestSingleton* _test_singleton;
-static Simulation* _simulation;
static Checksum* _checksum;
static LoadLocalisation* _load_localisation;
+static GameSingleton* _map_singleton;
-void initialize_openvic2_types(ModuleInitializationLevel p_level)
-{
+void initialize_openvic2_types(ModuleInitializationLevel p_level) {
if (p_level != MODULE_INITIALIZATION_LEVEL_SCENE) {
return;
}
- ClassDB::register_class<TestSingleton>();
- _test_singleton = memnew(TestSingleton);
- Engine::get_singleton()->register_singleton("TestSingleton", TestSingleton::get_singleton());
-
- ClassDB::register_class<Simulation>();
- _simulation = memnew(Simulation);
- Engine::get_singleton()->register_singleton("Simulation", Simulation::get_singleton());
-
ClassDB::register_class<Checksum>();
_checksum = memnew(Checksum);
Engine::get_singleton()->register_singleton("Checksum", Checksum::get_singleton());
@@ -40,6 +27,11 @@ void initialize_openvic2_types(ModuleInitializationLevel p_level)
_load_localisation = memnew(LoadLocalisation);
Engine::get_singleton()->register_singleton("LoadLocalisation", LoadLocalisation::get_singleton());
+ ClassDB::register_class<GameSingleton>();
+ _map_singleton = memnew(GameSingleton);
+ Engine::get_singleton()->register_singleton("GameSingleton", GameSingleton::get_singleton());
+
+ ClassDB::register_class<MapMesh>();
}
void uninitialize_openvic2_types(ModuleInitializationLevel p_level) {
@@ -47,26 +39,19 @@ void uninitialize_openvic2_types(ModuleInitializationLevel p_level) {
return;
}
- Engine::get_singleton()->unregister_singleton("TestSingleton");
- memdelete(_test_singleton);
-
- Engine::get_singleton()->unregister_singleton("Simulation");
- memdelete(_simulation);
-
Engine::get_singleton()->unregister_singleton("Checksum");
memdelete(_checksum);
Engine::get_singleton()->unregister_singleton("LoadLocalisation");
memdelete(_load_localisation);
-}
-extern "C"
-{
+ Engine::get_singleton()->unregister_singleton("GameSingleton");
+ memdelete(_map_singleton);
+}
+extern "C" {
// Initialization.
-
- GDExtensionBool GDE_EXPORT openvic2_library_init(const GDExtensionInterface *p_interface, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
- {
+ GDExtensionBool GDE_EXPORT openvic2_library_init(GDExtensionInterface const* p_interface, const GDExtensionClassLibraryPtr p_library, GDExtensionInitialization* r_initialization) {
GDExtensionBinding::InitObject init_obj(p_interface, p_library, r_initialization);
init_obj.register_initializer(initialize_openvic2_types);
@@ -75,4 +60,4 @@ extern "C"
return init_obj.init();
}
-} \ No newline at end of file
+}
diff --git a/extension/src/register_types.h b/extension/src/register_types.h
index 860359f..1bc96f8 100644
--- a/extension/src/register_types.h
+++ b/extension/src/register_types.h
@@ -1,6 +1,6 @@
#pragma once
-#include <godot_cpp/core/class_db.hpp>
+#include <godot_cpp/godot.hpp>
void initialize_openvic2_types(godot::ModuleInitializationLevel);
void uninitialize_openvic2_types(godot::ModuleInitializationLevel); \ No newline at end of file
diff --git a/game/art/economy/goods/Aeroplanes.png.import b/game/art/economy/goods/Aeroplanes.png.import
new file mode 100644
index 0000000..3d2451a
--- /dev/null
+++ b/game/art/economy/goods/Aeroplanes.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dv3expfaaeuln"
+path="res://.godot/imported/Aeroplanes.png-f64f9804b1d78b1cd5836ee405e25434.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Aeroplanes.png"
+dest_files=["res://.godot/imported/Aeroplanes.png-f64f9804b1d78b1cd5836ee405e25434.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Ammunition.png.import b/game/art/economy/goods/Ammunition.png.import
new file mode 100644
index 0000000..18de665
--- /dev/null
+++ b/game/art/economy/goods/Ammunition.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cdntq1bof843k"
+path="res://.godot/imported/Ammunition.png-2e4f424caeca5b92996fde71a1ab6ae6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Ammunition.png"
+dest_files=["res://.godot/imported/Ammunition.png-2e4f424caeca5b92996fde71a1ab6ae6.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Artillery.png.import b/game/art/economy/goods/Artillery.png.import
new file mode 100644
index 0000000..d14e6d0
--- /dev/null
+++ b/game/art/economy/goods/Artillery.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://brr6ui0bfenjx"
+path="res://.godot/imported/Artillery.png-e0bec54cd168f9abf3ab759e5d28a7d8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Artillery.png"
+dest_files=["res://.godot/imported/Artillery.png-e0bec54cd168f9abf3ab759e5d28a7d8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Automobiles.png.import b/game/art/economy/goods/Automobiles.png.import
new file mode 100644
index 0000000..4dbc7f3
--- /dev/null
+++ b/game/art/economy/goods/Automobiles.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dsbo2ysikx2pl"
+path="res://.godot/imported/Automobiles.png-2a9cf7764eccefabb2bf9877b31b9df4.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Automobiles.png"
+dest_files=["res://.godot/imported/Automobiles.png-2a9cf7764eccefabb2bf9877b31b9df4.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/CannedFood.png.import b/game/art/economy/goods/CannedFood.png.import
new file mode 100644
index 0000000..4ef0744
--- /dev/null
+++ b/game/art/economy/goods/CannedFood.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bohmpug1q1055"
+path="res://.godot/imported/CannedFood.png-7e9c65b73180ffd48293ab7044e59b65.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/CannedFood.png"
+dest_files=["res://.godot/imported/CannedFood.png-7e9c65b73180ffd48293ab7044e59b65.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Cattle.png.import b/game/art/economy/goods/Cattle.png.import
new file mode 100644
index 0000000..203e9c9
--- /dev/null
+++ b/game/art/economy/goods/Cattle.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://qvjxhaoul0hd"
+path="res://.godot/imported/Cattle.png-0ef679bb3a47dc39d334f87f1f7f33d0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Cattle.png"
+dest_files=["res://.godot/imported/Cattle.png-0ef679bb3a47dc39d334f87f1f7f33d0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Cement.png.import b/game/art/economy/goods/Cement.png.import
new file mode 100644
index 0000000..00dde5e
--- /dev/null
+++ b/game/art/economy/goods/Cement.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://u81g2lk4fdmw"
+path="res://.godot/imported/Cement.png-7e727795f0350e73eddec47889a21c5e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Cement.png"
+dest_files=["res://.godot/imported/Cement.png-7e727795f0350e73eddec47889a21c5e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/ClipperConvoys.png.import b/game/art/economy/goods/ClipperConvoys.png.import
new file mode 100644
index 0000000..581ad9e
--- /dev/null
+++ b/game/art/economy/goods/ClipperConvoys.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dlvkgmqv1lvu3"
+path="res://.godot/imported/ClipperConvoys.png-159028b2922e35ecf2dcbfccd801cf86.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/ClipperConvoys.png"
+dest_files=["res://.godot/imported/ClipperConvoys.png-159028b2922e35ecf2dcbfccd801cf86.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Coal.png.import b/game/art/economy/goods/Coal.png.import
new file mode 100644
index 0000000..13de37f
--- /dev/null
+++ b/game/art/economy/goods/Coal.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d05ib0dx1ybw3"
+path="res://.godot/imported/Coal.png-4c471088e5b174c53d56febb417d0ea3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Coal.png"
+dest_files=["res://.godot/imported/Coal.png-4c471088e5b174c53d56febb417d0ea3.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Coffee.png.import b/game/art/economy/goods/Coffee.png.import
new file mode 100644
index 0000000..0c32849
--- /dev/null
+++ b/game/art/economy/goods/Coffee.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dmb4lygh1p05u"
+path="res://.godot/imported/Coffee.png-4e4c1279a8965c64feba98816229f183.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Coffee.png"
+dest_files=["res://.godot/imported/Coffee.png-4e4c1279a8965c64feba98816229f183.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Cotton.png.import b/game/art/economy/goods/Cotton.png.import
new file mode 100644
index 0000000..8d0b816
--- /dev/null
+++ b/game/art/economy/goods/Cotton.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://beh58y5y1rulq"
+path="res://.godot/imported/Cotton.png-e7a9a26df0e3cb09e7f774048297a99b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Cotton.png"
+dest_files=["res://.godot/imported/Cotton.png-e7a9a26df0e3cb09e7f774048297a99b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Dye.png.import b/game/art/economy/goods/Dye.png.import
new file mode 100644
index 0000000..5c74456
--- /dev/null
+++ b/game/art/economy/goods/Dye.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://6cj4nyn2ox0i"
+path="res://.godot/imported/Dye.png-95addae97d31427aaf5a96fdc20b28b7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Dye.png"
+dest_files=["res://.godot/imported/Dye.png-95addae97d31427aaf5a96fdc20b28b7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/ElectricGear.png.import b/game/art/economy/goods/ElectricGear.png.import
new file mode 100644
index 0000000..a0bb59a
--- /dev/null
+++ b/game/art/economy/goods/ElectricGear.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://fumygkymqhit"
+path="res://.godot/imported/ElectricGear.png-8a9b67b515c96e0a0971c4c9eac2278d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/ElectricGear.png"
+dest_files=["res://.godot/imported/ElectricGear.png-8a9b67b515c96e0a0971c4c9eac2278d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Explosives.png.import b/game/art/economy/goods/Explosives.png.import
new file mode 100644
index 0000000..6edde00
--- /dev/null
+++ b/game/art/economy/goods/Explosives.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c34svjqn86f3s"
+path="res://.godot/imported/Explosives.png-d7cbb8033e9cf83a94115ff58e1cceec.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Explosives.png"
+dest_files=["res://.godot/imported/Explosives.png-d7cbb8033e9cf83a94115ff58e1cceec.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Fabric.png.import b/game/art/economy/goods/Fabric.png.import
new file mode 100644
index 0000000..557da36
--- /dev/null
+++ b/game/art/economy/goods/Fabric.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bgch0t83ofxt"
+path="res://.godot/imported/Fabric.png-f640354692635eb1afd109feef624b45.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Fabric.png"
+dest_files=["res://.godot/imported/Fabric.png-f640354692635eb1afd109feef624b45.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Fertilizer.png.import b/game/art/economy/goods/Fertilizer.png.import
new file mode 100644
index 0000000..068e533
--- /dev/null
+++ b/game/art/economy/goods/Fertilizer.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dy46mh5o0dyyf"
+path="res://.godot/imported/Fertilizer.png-c2a9407f34cedd7f2bbd3614c6070ab8.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Fertilizer.png"
+dest_files=["res://.godot/imported/Fertilizer.png-c2a9407f34cedd7f2bbd3614c6070ab8.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Fish.png.import b/game/art/economy/goods/Fish.png.import
new file mode 100644
index 0000000..9cca12b
--- /dev/null
+++ b/game/art/economy/goods/Fish.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://u42bt5v5vx42"
+path="res://.godot/imported/Fish.png-c8cef591ba5252382b7f603bee886146.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Fish.png"
+dest_files=["res://.godot/imported/Fish.png-c8cef591ba5252382b7f603bee886146.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Fruit.png.import b/game/art/economy/goods/Fruit.png.import
new file mode 100644
index 0000000..059b488
--- /dev/null
+++ b/game/art/economy/goods/Fruit.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cfr8rxa43dhfk"
+path="res://.godot/imported/Fruit.png-6e3c553c5a8bd67d505ae0751d391712.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Fruit.png"
+dest_files=["res://.godot/imported/Fruit.png-6e3c553c5a8bd67d505ae0751d391712.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Fuel.png.import b/game/art/economy/goods/Fuel.png.import
new file mode 100644
index 0000000..9584b8d
--- /dev/null
+++ b/game/art/economy/goods/Fuel.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bcmw4c8vcjt1w"
+path="res://.godot/imported/Fuel.png-d4cce318e3b1cf2b2846d0bf589b2e56.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Fuel.png"
+dest_files=["res://.godot/imported/Fuel.png-d4cce318e3b1cf2b2846d0bf589b2e56.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Furniture.png.import b/game/art/economy/goods/Furniture.png.import
new file mode 100644
index 0000000..422ea21
--- /dev/null
+++ b/game/art/economy/goods/Furniture.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ccvpkwsm0gn0i"
+path="res://.godot/imported/Furniture.png-116e7f7a3708afa2cee87e79151e10ef.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Furniture.png"
+dest_files=["res://.godot/imported/Furniture.png-116e7f7a3708afa2cee87e79151e10ef.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Glass.png.import b/game/art/economy/goods/Glass.png.import
new file mode 100644
index 0000000..54383f6
--- /dev/null
+++ b/game/art/economy/goods/Glass.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b0qwt4tbqic4q"
+path="res://.godot/imported/Glass.png-a801ee62345932edc9b2cf10740cc3fe.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Glass.png"
+dest_files=["res://.godot/imported/Glass.png-a801ee62345932edc9b2cf10740cc3fe.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Grain.png.import b/game/art/economy/goods/Grain.png.import
new file mode 100644
index 0000000..f1ca97a
--- /dev/null
+++ b/game/art/economy/goods/Grain.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cp45p4adg03ia"
+path="res://.godot/imported/Grain.png-9ca7aa6f52786e3b8c0755fe3b032801.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Grain.png"
+dest_files=["res://.godot/imported/Grain.png-9ca7aa6f52786e3b8c0755fe3b032801.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Iron.png.import b/game/art/economy/goods/Iron.png.import
new file mode 100644
index 0000000..4e0c276
--- /dev/null
+++ b/game/art/economy/goods/Iron.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://deubji56a8101"
+path="res://.godot/imported/Iron.png-bc3abc240fd156b77f46e8b04178d267.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Iron.png"
+dest_files=["res://.godot/imported/Iron.png-bc3abc240fd156b77f46e8b04178d267.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Liquor.png.import b/game/art/economy/goods/Liquor.png.import
new file mode 100644
index 0000000..20d0eb2
--- /dev/null
+++ b/game/art/economy/goods/Liquor.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://f4kjlktvm2r2"
+path="res://.godot/imported/Liquor.png-10c1257a0d7236955a0274bbd88341ba.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Liquor.png"
+dest_files=["res://.godot/imported/Liquor.png-10c1257a0d7236955a0274bbd88341ba.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Lumber.png.import b/game/art/economy/goods/Lumber.png.import
new file mode 100644
index 0000000..d93954a
--- /dev/null
+++ b/game/art/economy/goods/Lumber.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ba7mptrphkaqd"
+path="res://.godot/imported/Lumber.png-85cdb47c3db3dee028b541aa407ce5bc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Lumber.png"
+dest_files=["res://.godot/imported/Lumber.png-85cdb47c3db3dee028b541aa407ce5bc.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/LuxuryClothes.png.import b/game/art/economy/goods/LuxuryClothes.png.import
new file mode 100644
index 0000000..52b7adb
--- /dev/null
+++ b/game/art/economy/goods/LuxuryClothes.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ce3wvhvqcy1d2"
+path="res://.godot/imported/LuxuryClothes.png-3d5d3b7653f1bcfe4c45e96265494a94.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/LuxuryClothes.png"
+dest_files=["res://.godot/imported/LuxuryClothes.png-3d5d3b7653f1bcfe4c45e96265494a94.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/LuxuryFurniture.png.import b/game/art/economy/goods/LuxuryFurniture.png.import
new file mode 100644
index 0000000..4b8a812
--- /dev/null
+++ b/game/art/economy/goods/LuxuryFurniture.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ckrs01pbwvk1j"
+path="res://.godot/imported/LuxuryFurniture.png-8414a4d701c4ceb3bb78500f81eb9f1c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/LuxuryFurniture.png"
+dest_files=["res://.godot/imported/LuxuryFurniture.png-8414a4d701c4ceb3bb78500f81eb9f1c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/MachineParts.png.import b/game/art/economy/goods/MachineParts.png.import
new file mode 100644
index 0000000..2972256
--- /dev/null
+++ b/game/art/economy/goods/MachineParts.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dyssc4fyvg0i0"
+path="res://.godot/imported/MachineParts.png-31ee9b4bc155ba76c9068820493dc7c5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/MachineParts.png"
+dest_files=["res://.godot/imported/MachineParts.png-31ee9b4bc155ba76c9068820493dc7c5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Oil.png.import b/game/art/economy/goods/Oil.png.import
new file mode 100644
index 0000000..464660a
--- /dev/null
+++ b/game/art/economy/goods/Oil.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c05in6w2pfu7a"
+path="res://.godot/imported/Oil.png-822e9dd049ab1453614ebbfdd1b38454.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Oil.png"
+dest_files=["res://.godot/imported/Oil.png-822e9dd049ab1453614ebbfdd1b38454.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Opium.png.import b/game/art/economy/goods/Opium.png.import
new file mode 100644
index 0000000..d9bbe15
--- /dev/null
+++ b/game/art/economy/goods/Opium.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dn1ex6s23wjvd"
+path="res://.godot/imported/Opium.png-e336924c62c7eb8e056df2ecd312f451.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Opium.png"
+dest_files=["res://.godot/imported/Opium.png-e336924c62c7eb8e056df2ecd312f451.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Paper.png.import b/game/art/economy/goods/Paper.png.import
new file mode 100644
index 0000000..3cc304d
--- /dev/null
+++ b/game/art/economy/goods/Paper.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://evev4ofp0yv7"
+path="res://.godot/imported/Paper.png-ec2a07dd74f449d1555735b89127e044.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Paper.png"
+dest_files=["res://.godot/imported/Paper.png-ec2a07dd74f449d1555735b89127e044.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/PreciousMetal.png.import b/game/art/economy/goods/PreciousMetal.png.import
new file mode 100644
index 0000000..726b023
--- /dev/null
+++ b/game/art/economy/goods/PreciousMetal.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://copn3xhj12vwc"
+path="res://.godot/imported/PreciousMetal.png-27348530d09ac15718b658314d037f79.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/PreciousMetal.png"
+dest_files=["res://.godot/imported/PreciousMetal.png-27348530d09ac15718b658314d037f79.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Radios.png.import b/game/art/economy/goods/Radios.png.import
new file mode 100644
index 0000000..bb91ffd
--- /dev/null
+++ b/game/art/economy/goods/Radios.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c8e3casrgmmot"
+path="res://.godot/imported/Radios.png-5025dbef9a0f9e4874cc904b557186c3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Radios.png"
+dest_files=["res://.godot/imported/Radios.png-5025dbef9a0f9e4874cc904b557186c3.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/RegularClothes.png.import b/game/art/economy/goods/RegularClothes.png.import
new file mode 100644
index 0000000..4988d39
--- /dev/null
+++ b/game/art/economy/goods/RegularClothes.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d2rt64gc6vn8w"
+path="res://.godot/imported/RegularClothes.png-bb54e5df7996148886ae0a19afbbcaaf.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/RegularClothes.png"
+dest_files=["res://.godot/imported/RegularClothes.png-bb54e5df7996148886ae0a19afbbcaaf.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Rubber.png.import b/game/art/economy/goods/Rubber.png.import
new file mode 100644
index 0000000..80fde27
--- /dev/null
+++ b/game/art/economy/goods/Rubber.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cgvppu138phcu"
+path="res://.godot/imported/Rubber.png-fb0013e7ac71289b464202fbfe17a41c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Rubber.png"
+dest_files=["res://.godot/imported/Rubber.png-fb0013e7ac71289b464202fbfe17a41c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Silk.png.import b/game/art/economy/goods/Silk.png.import
new file mode 100644
index 0000000..adb2acc
--- /dev/null
+++ b/game/art/economy/goods/Silk.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dbc1lb1aqxrok"
+path="res://.godot/imported/Silk.png-013589539223b8fc4be0f091282266b0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Silk.png"
+dest_files=["res://.godot/imported/Silk.png-013589539223b8fc4be0f091282266b0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/SmallArms.png.import b/game/art/economy/goods/SmallArms.png.import
new file mode 100644
index 0000000..cd68f36
--- /dev/null
+++ b/game/art/economy/goods/SmallArms.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://sxwrp1o87mad"
+path="res://.godot/imported/SmallArms.png-f90e4e981a7cc5edb84c09bfd86d45f6.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/SmallArms.png"
+dest_files=["res://.godot/imported/SmallArms.png-f90e4e981a7cc5edb84c09bfd86d45f6.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/SteamerConvoys.png.import b/game/art/economy/goods/SteamerConvoys.png.import
new file mode 100644
index 0000000..2df38b7
--- /dev/null
+++ b/game/art/economy/goods/SteamerConvoys.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ixlxhne7762f"
+path="res://.godot/imported/SteamerConvoys.png-a0f19ac48200d954be7e73f961899ebc.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/SteamerConvoys.png"
+dest_files=["res://.godot/imported/SteamerConvoys.png-a0f19ac48200d954be7e73f961899ebc.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Steel.png.import b/game/art/economy/goods/Steel.png.import
new file mode 100644
index 0000000..3fbb5cc
--- /dev/null
+++ b/game/art/economy/goods/Steel.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://u72ecpwc4ued"
+path="res://.godot/imported/Steel.png-8640de27c835c010f4ae43926b750ebb.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Steel.png"
+dest_files=["res://.godot/imported/Steel.png-8640de27c835c010f4ae43926b750ebb.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Sulphur.png.import b/game/art/economy/goods/Sulphur.png.import
new file mode 100644
index 0000000..2b7c7b0
--- /dev/null
+++ b/game/art/economy/goods/Sulphur.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://di5qdxsgmmeso"
+path="res://.godot/imported/Sulphur.png-51f6a765f564214878392306f69a5a1e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Sulphur.png"
+dest_files=["res://.godot/imported/Sulphur.png-51f6a765f564214878392306f69a5a1e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Tanks.png.import b/game/art/economy/goods/Tanks.png.import
new file mode 100644
index 0000000..cd8a225
--- /dev/null
+++ b/game/art/economy/goods/Tanks.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dgdmiswjye8bc"
+path="res://.godot/imported/Tanks.png-cff91aa93834fa6ee4ae3072bfe8ba65.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Tanks.png"
+dest_files=["res://.godot/imported/Tanks.png-cff91aa93834fa6ee4ae3072bfe8ba65.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Tea.png.import b/game/art/economy/goods/Tea.png.import
new file mode 100644
index 0000000..1a8ccbb
--- /dev/null
+++ b/game/art/economy/goods/Tea.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c0jadcjnk8baj"
+path="res://.godot/imported/Tea.png-ba317b3403a4b7c4cf85c588e48d7f37.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Tea.png"
+dest_files=["res://.godot/imported/Tea.png-ba317b3403a4b7c4cf85c588e48d7f37.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Telephones.png.import b/game/art/economy/goods/Telephones.png.import
new file mode 100644
index 0000000..f69e37b
--- /dev/null
+++ b/game/art/economy/goods/Telephones.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b2lefnj8r8csk"
+path="res://.godot/imported/Telephones.png-96111950d02e51d0a7b4a3da0be5a8b7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Telephones.png"
+dest_files=["res://.godot/imported/Telephones.png-96111950d02e51d0a7b4a3da0be5a8b7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Timber.png.import b/game/art/economy/goods/Timber.png.import
new file mode 100644
index 0000000..ef2d1c4
--- /dev/null
+++ b/game/art/economy/goods/Timber.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bvmajoybi4mpx"
+path="res://.godot/imported/Timber.png-6bbb5df327d5f86e822a9573fafe2559.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Timber.png"
+dest_files=["res://.godot/imported/Timber.png-6bbb5df327d5f86e822a9573fafe2559.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Tobacco.png.import b/game/art/economy/goods/Tobacco.png.import
new file mode 100644
index 0000000..2f3b067
--- /dev/null
+++ b/game/art/economy/goods/Tobacco.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://duywblje3mtuc"
+path="res://.godot/imported/Tobacco.png-15d944a475ff2581c18e69ab81117162.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Tobacco.png"
+dest_files=["res://.godot/imported/Tobacco.png-15d944a475ff2581c18e69ab81117162.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/TropicalWood.png.import b/game/art/economy/goods/TropicalWood.png.import
new file mode 100644
index 0000000..e527cc3
--- /dev/null
+++ b/game/art/economy/goods/TropicalWood.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bl4y0v6iqigsm"
+path="res://.godot/imported/TropicalWood.png-b034631744d40b9c6708952b6226ae05.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/TropicalWood.png"
+dest_files=["res://.godot/imported/TropicalWood.png-b034631744d40b9c6708952b6226ae05.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Wine.png.import b/game/art/economy/goods/Wine.png.import
new file mode 100644
index 0000000..d639f2d
--- /dev/null
+++ b/game/art/economy/goods/Wine.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c5b1mst7e148p"
+path="res://.godot/imported/Wine.png-54a8f9decc5c6357daf1bff489288e2d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Wine.png"
+dest_files=["res://.godot/imported/Wine.png-54a8f9decc5c6357daf1bff489288e2d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/economy/goods/Wool.png.import b/game/art/economy/goods/Wool.png.import
new file mode 100644
index 0000000..a004986
--- /dev/null
+++ b/game/art/economy/goods/Wool.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://yfj36hk5ikvw"
+path="res://.godot/imported/Wool.png-8d246d932f235d68b783ad3d290285cb.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/economy/goods/Wool.png"
+dest_files=["res://.godot/imported/Wool.png-8d246d932f235d68b783ad3d290285cb.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Army Decision Making.png.import b/game/art/technology/army/Army Decision Making.png.import
new file mode 100644
index 0000000..736bee3
--- /dev/null
+++ b/game/art/technology/army/Army Decision Making.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dckthf8dux76m"
+path="res://.godot/imported/Army Decision Making.png-de56087375aabbf13116266546faa432.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Army Decision Making.png"
+dest_files=["res://.godot/imported/Army Decision Making.png-de56087375aabbf13116266546faa432.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Army NCO Training.png.import b/game/art/technology/army/Army NCO Training.png.import
new file mode 100644
index 0000000..b12590b
--- /dev/null
+++ b/game/art/technology/army/Army NCO Training.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ctfu58uqxj5pf"
+path="res://.godot/imported/Army NCO Training.png-821f0f46c556e9cf3b61fa3790f53961.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Army NCO Training.png"
+dest_files=["res://.godot/imported/Army NCO Training.png-821f0f46c556e9cf3b61fa3790f53961.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Army Professionalism.png.import b/game/art/technology/army/Army Professionalism.png.import
new file mode 100644
index 0000000..5ca9b32
--- /dev/null
+++ b/game/art/technology/army/Army Professionalism.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://43kk6efdupms"
+path="res://.godot/imported/Army Professionalism.png-3303d162aeb25fa8cead9cd12a151bdf.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Army Professionalism.png"
+dest_files=["res://.godot/imported/Army Professionalism.png-3303d162aeb25fa8cead9cd12a151bdf.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Army Risk Management.png.import b/game/art/technology/army/Army Risk Management.png.import
new file mode 100644
index 0000000..7e3af00
--- /dev/null
+++ b/game/art/technology/army/Army Risk Management.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dxjxl36ycaagx"
+path="res://.godot/imported/Army Risk Management.png-d4c5a789328eafcefc15f4290af86b0d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Army Risk Management.png"
+dest_files=["res://.godot/imported/Army Risk Management.png-d4c5a789328eafcefc15f4290af86b0d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Bolt-action Rifle.png.import b/game/art/technology/army/Bolt-action Rifle.png.import
new file mode 100644
index 0000000..dd05df0
--- /dev/null
+++ b/game/art/technology/army/Bolt-action Rifle.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dnbo2i4corwsf"
+path="res://.godot/imported/Bolt-action Rifle.png-4952029366c5c5fa7365f610175bc54d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Bolt-action Rifle.png"
+dest_files=["res://.godot/imported/Bolt-action Rifle.png-4952029366c5c5fa7365f610175bc54d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Breech-Loaded Rifles.png.import b/game/art/technology/army/Breech-Loaded Rifles.png.import
new file mode 100644
index 0000000..15eb654
--- /dev/null
+++ b/game/art/technology/army/Breech-Loaded Rifles.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dri3wrewyf13r"
+path="res://.godot/imported/Breech-Loaded Rifles.png-0a1a7219ca3c5265d85a580eb205a6c1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Breech-Loaded Rifles.png"
+dest_files=["res://.godot/imported/Breech-Loaded Rifles.png-0a1a7219ca3c5265d85a580eb205a6c1.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Bronze Muzzle-loaded Artillery.png.import b/game/art/technology/army/Bronze Muzzle-loaded Artillery.png.import
new file mode 100644
index 0000000..7baa8c4
--- /dev/null
+++ b/game/art/technology/army/Bronze Muzzle-loaded Artillery.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bx2w63j11of83"
+path="res://.godot/imported/Bronze Muzzle-loaded Artillery.png-a206f30df41008ce524d81461e3f725a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Bronze Muzzle-loaded Artillery.png"
+dest_files=["res://.godot/imported/Bronze Muzzle-loaded Artillery.png-a206f30df41008ce524d81461e3f725a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Deep Defense System.png.import b/game/art/technology/army/Deep Defense System.png.import
new file mode 100644
index 0000000..810c88b
--- /dev/null
+++ b/game/art/technology/army/Deep Defense System.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dihxim0oemat6"
+path="res://.godot/imported/Deep Defense System.png-1ef6b4036bbfd0934f46f40fb02d30ac.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Deep Defense System.png"
+dest_files=["res://.godot/imported/Deep Defense System.png-1ef6b4036bbfd0934f46f40fb02d30ac.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Flintlock Rifles.png.import b/game/art/technology/army/Flintlock Rifles.png.import
new file mode 100644
index 0000000..65894cd
--- /dev/null
+++ b/game/art/technology/army/Flintlock Rifles.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://786ytfkafkyd"
+path="res://.godot/imported/Flintlock Rifles.png-59fc48db6543edee17194c6fd6c10eef.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Flintlock Rifles.png"
+dest_files=["res://.godot/imported/Flintlock Rifles.png-59fc48db6543edee17194c6fd6c10eef.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Indirect Artillery Fire.png.import b/game/art/technology/army/Indirect Artillery Fire.png.import
new file mode 100644
index 0000000..50931c0
--- /dev/null
+++ b/game/art/technology/army/Indirect Artillery Fire.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b8xmx42ik0nhs"
+path="res://.godot/imported/Indirect Artillery Fire.png-8d6eaed9cd7edcdbef53d4d1251ed9f7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Indirect Artillery Fire.png"
+dest_files=["res://.godot/imported/Indirect Artillery Fire.png-8d6eaed9cd7edcdbef53d4d1251ed9f7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Infiltration.png.import b/game/art/technology/army/Infiltration.png.import
new file mode 100644
index 0000000..a6697b3
--- /dev/null
+++ b/game/art/technology/army/Infiltration.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bsfdmmu60tc3n"
+path="res://.godot/imported/Infiltration.png-57a5e43cb820964cecd846278423621f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Infiltration.png"
+dest_files=["res://.godot/imported/Infiltration.png-57a5e43cb820964cecd846278423621f.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Iron Breech-Loaded Artillery.png.import b/game/art/technology/army/Iron Breech-Loaded Artillery.png.import
new file mode 100644
index 0000000..993627c
--- /dev/null
+++ b/game/art/technology/army/Iron Breech-Loaded Artillery.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bbsx7rfndoftn"
+path="res://.godot/imported/Iron Breech-Loaded Artillery.png-4bac252ba7934897b162ed696b4b7509.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Iron Breech-Loaded Artillery.png"
+dest_files=["res://.godot/imported/Iron Breech-Loaded Artillery.png-4bac252ba7934897b162ed696b4b7509.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Iron Muzzle-Loaded Artillery.png.import b/game/art/technology/army/Iron Muzzle-Loaded Artillery.png.import
new file mode 100644
index 0000000..efa4e51
--- /dev/null
+++ b/game/art/technology/army/Iron Muzzle-Loaded Artillery.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cq4kommomraba"
+path="res://.godot/imported/Iron Muzzle-Loaded Artillery.png-5777c093343e4ec0cb88b085c086a1f7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Iron Muzzle-Loaded Artillery.png"
+dest_files=["res://.godot/imported/Iron Muzzle-Loaded Artillery.png-5777c093343e4ec0cb88b085c086a1f7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Machine Gun.png.import b/game/art/technology/army/Machine Gun.png.import
new file mode 100644
index 0000000..f6f3775
--- /dev/null
+++ b/game/art/technology/army/Machine Gun.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cgnd44ys1m4cf"
+path="res://.godot/imported/Machine Gun.png-e853bb6291451d02084e1823c7a0d98f.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Machine Gun.png"
+dest_files=["res://.godot/imported/Machine Gun.png-e853bb6291451d02084e1823c7a0d98f.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Military Directionism.png.import b/game/art/technology/army/Military Directionism.png.import
new file mode 100644
index 0000000..17376e1
--- /dev/null
+++ b/game/art/technology/army/Military Directionism.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dgx61xfpng6ka"
+path="res://.godot/imported/Military Directionism.png-565e1e6381fc0f0d7fcb9195f249ca1a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Military Directionism.png"
+dest_files=["res://.godot/imported/Military Directionism.png-565e1e6381fc0f0d7fcb9195f249ca1a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Military Logistics.png.import b/game/art/technology/army/Military Logistics.png.import
new file mode 100644
index 0000000..0326305
--- /dev/null
+++ b/game/art/technology/army/Military Logistics.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ctoh8oq6afuki"
+path="res://.godot/imported/Military Logistics.png-5748d86c74b85e45d6ee2d2f8d81f95a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Military Logistics.png"
+dest_files=["res://.godot/imported/Military Logistics.png-5748d86c74b85e45d6ee2d2f8d81f95a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Military Plans.png.import b/game/art/technology/army/Military Plans.png.import
new file mode 100644
index 0000000..d8e8b0a
--- /dev/null
+++ b/game/art/technology/army/Military Plans.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bslr0m3ca74y6"
+path="res://.godot/imported/Military Plans.png-1a32d460e676e358dde057d988d707ac.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Military Plans.png"
+dest_files=["res://.godot/imported/Military Plans.png-1a32d460e676e358dde057d988d707ac.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Military Staff System.png.import b/game/art/technology/army/Military Staff System.png.import
new file mode 100644
index 0000000..844df8f
--- /dev/null
+++ b/game/art/technology/army/Military Staff System.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bj6kw2ou4aoop"
+path="res://.godot/imported/Military Staff System.png-094a056d8c546ed0da41d1f1dbcdb2a1.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Military Staff System.png"
+dest_files=["res://.godot/imported/Military Staff System.png-094a056d8c546ed0da41d1f1dbcdb2a1.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Military Statistics.png.import b/game/art/technology/army/Military Statistics.png.import
new file mode 100644
index 0000000..e495bd9
--- /dev/null
+++ b/game/art/technology/army/Military Statistics.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c8oww2lsc5sfe"
+path="res://.godot/imported/Military Statistics.png-4941c660e22242166acf00038144df37.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Military Statistics.png"
+dest_files=["res://.godot/imported/Military Statistics.png-4941c660e22242166acf00038144df37.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Muzzle-loaded Rifles.png.import b/game/art/technology/army/Muzzle-loaded Rifles.png.import
new file mode 100644
index 0000000..5c79afd
--- /dev/null
+++ b/game/art/technology/army/Muzzle-loaded Rifles.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c1n4hwwt4jwv0"
+path="res://.godot/imported/Muzzle-loaded Rifles.png-37ff916f6889925dc68e88b1251f14d7.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Muzzle-loaded Rifles.png"
+dest_files=["res://.godot/imported/Muzzle-loaded Rifles.png-37ff916f6889925dc68e88b1251f14d7.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Point Defense System.png.import b/game/art/technology/army/Point Defense System.png.import
new file mode 100644
index 0000000..b5b5b1c
--- /dev/null
+++ b/game/art/technology/army/Point Defense System.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ckfv5gbcfij5m"
+path="res://.godot/imported/Point Defense System.png-cdd4b2bbbf74fe5b0ebe7d2306d9d468.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Point Defense System.png"
+dest_files=["res://.godot/imported/Point Defense System.png-cdd4b2bbbf74fe5b0ebe7d2306d9d468.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Post-Napoleonic Thought.png.import b/game/art/technology/army/Post-Napoleonic Thought.png.import
new file mode 100644
index 0000000..2e963fc
--- /dev/null
+++ b/game/art/technology/army/Post-Napoleonic Thought.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bw2bidtorexob"
+path="res://.godot/imported/Post-Napoleonic Thought.png-0c345d9286a8a1fd4b7bcbae60ec0250.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Post-Napoleonic Thought.png"
+dest_files=["res://.godot/imported/Post-Napoleonic Thought.png-0c345d9286a8a1fd4b7bcbae60ec0250.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Steel Breech-Loaded Artillery.png.import b/game/art/technology/army/Steel Breech-Loaded Artillery.png.import
new file mode 100644
index 0000000..92e374e
--- /dev/null
+++ b/game/art/technology/army/Steel Breech-Loaded Artillery.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cx15ctmi52kv1"
+path="res://.godot/imported/Steel Breech-Loaded Artillery.png-2fdc9e71a5204d6d657b8ca9b4cfb3a0.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Steel Breech-Loaded Artillery.png"
+dest_files=["res://.godot/imported/Steel Breech-Loaded Artillery.png-2fdc9e71a5204d6d657b8ca9b4cfb3a0.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/Strategic Mobility.png.import b/game/art/technology/army/Strategic Mobility.png.import
new file mode 100644
index 0000000..890b896
--- /dev/null
+++ b/game/art/technology/army/Strategic Mobility.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cac2g1uui35d5"
+path="res://.godot/imported/Strategic Mobility.png-ceaba1342f52e5251521c79dcbf79951.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/Strategic Mobility.png"
+dest_files=["res://.godot/imported/Strategic Mobility.png-ceaba1342f52e5251521c79dcbf79951.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/technology/army/The Command Principle.png.import b/game/art/technology/army/The Command Principle.png.import
new file mode 100644
index 0000000..94c991f
--- /dev/null
+++ b/game/art/technology/army/The Command Principle.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://r3vxso20jcuf"
+path="res://.godot/imported/The Command Principle.png-def1e8cd5ae5294984c171ffa624d74d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/technology/army/The Command Principle.png"
+dest_files=["res://.godot/imported/The Command Principle.png-def1e8cd5ae5294984c171ffa624d74d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/terrain/farmlands.png b/game/art/terrain/farmlands.png
new file mode 100644
index 0000000..3702ad0
--- /dev/null
+++ b/game/art/terrain/farmlands.png
Binary files differ
diff --git a/game/art/terrain/farmlands.png.import b/game/art/terrain/farmlands.png.import
new file mode 100644
index 0000000..4a06b1f
--- /dev/null
+++ b/game/art/terrain/farmlands.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ckf222w5usrsu"
+path="res://.godot/imported/farmlands.png-821213ab9dba19cea6f6c966b3a3760b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/terrain/farmlands.png"
+dest_files=["res://.godot/imported/farmlands.png-821213ab9dba19cea6f6c966b3a3760b.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=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=false
+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/art/ui/minimap.png b/game/art/ui/minimap.png
new file mode 100644
index 0000000..9922b28
--- /dev/null
+++ b/game/art/ui/minimap.png
Binary files differ
diff --git a/game/art/ui/minimap.png.import b/game/art/ui/minimap.png.import
new file mode 100644
index 0000000..36f22e7
--- /dev/null
+++ b/game/art/ui/minimap.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c0sm1jfu4kyv3"
+path="res://.godot/imported/minimap.png-05ee44469856e77fd05107fe9e259e25.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/ui/minimap.png"
+dest_files=["res://.godot/imported/minimap.png-05ee44469856e77fd05107fe9e259e25.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=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=false
+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/art/ui/minimap_frame.png b/game/art/ui/minimap_frame.png
new file mode 100644
index 0000000..21aa2ce
--- /dev/null
+++ b/game/art/ui/minimap_frame.png
Binary files differ
diff --git a/game/art/ui/minimap_frame.png.import b/game/art/ui/minimap_frame.png.import
new file mode 100644
index 0000000..3d2aeee
--- /dev/null
+++ b/game/art/ui/minimap_frame.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://vr1hq2stk8ny"
+path="res://.godot/imported/minimap_frame.png-3668393435d2582fb1dc8a9598573e80.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/ui/minimap_frame.png"
+dest_files=["res://.godot/imported/minimap_frame.png-3668393435d2582fb1dc8a9598573e80.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=false
+mipmaps/limit=-1
+roughness/mode=0
+roughness/src_normal=""
+process/fix_alpha_border=false
+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/art/units/Airplane.png.import b/game/art/units/Airplane.png.import
new file mode 100644
index 0000000..830c140
--- /dev/null
+++ b/game/art/units/Airplane.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b05nq06fahtxi"
+path="res://.godot/imported/Airplane.png-41f37b8f11a65e540eed98b24a297027.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Airplane.png"
+dest_files=["res://.godot/imported/Airplane.png-41f37b8f11a65e540eed98b24a297027.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Armor.png.import b/game/art/units/Armor.png.import
new file mode 100644
index 0000000..42c807b
--- /dev/null
+++ b/game/art/units/Armor.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cr6bw4jwrydpm"
+path="res://.godot/imported/Armor.png-748c76c2a00bae3705715d74bcf34c0b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Armor.png"
+dest_files=["res://.godot/imported/Armor.png-748c76c2a00bae3705715d74bcf34c0b.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Artillery.png.import b/game/art/units/Artillery.png.import
new file mode 100644
index 0000000..5775e85
--- /dev/null
+++ b/game/art/units/Artillery.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://6q1scxdbxppv"
+path="res://.godot/imported/Artillery.png-a510c953fca66ca6ad75d3de82213d2d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Artillery.png"
+dest_files=["res://.godot/imported/Artillery.png-a510c953fca66ca6ad75d3de82213d2d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Battleship.png.import b/game/art/units/Battleship.png.import
new file mode 100644
index 0000000..aa56ee7
--- /dev/null
+++ b/game/art/units/Battleship.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://j16k5y0gnkqy"
+path="res://.godot/imported/Battleship.png-ea7ecf7a355e55b030c9c7a6b1765d4e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Battleship.png"
+dest_files=["res://.godot/imported/Battleship.png-ea7ecf7a355e55b030c9c7a6b1765d4e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Cavalry.png.import b/game/art/units/Cavalry.png.import
new file mode 100644
index 0000000..7cc22ed
--- /dev/null
+++ b/game/art/units/Cavalry.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bmv2431sb27kw"
+path="res://.godot/imported/Cavalry.png-e9b856b6c9804e5a46f98e084fb8322d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Cavalry.png"
+dest_files=["res://.godot/imported/Cavalry.png-e9b856b6c9804e5a46f98e084fb8322d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/ClipperTransport.png.import b/game/art/units/ClipperTransport.png.import
new file mode 100644
index 0000000..aa42ffd
--- /dev/null
+++ b/game/art/units/ClipperTransport.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bac3o2bng7o3y"
+path="res://.godot/imported/ClipperTransport.png-9e61055011e1905d88dbc935d3804c84.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/ClipperTransport.png"
+dest_files=["res://.godot/imported/ClipperTransport.png-9e61055011e1905d88dbc935d3804c84.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/CommerceRaider.png.import b/game/art/units/CommerceRaider.png.import
new file mode 100644
index 0000000..9659365
--- /dev/null
+++ b/game/art/units/CommerceRaider.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://df6xi485nd40t"
+path="res://.godot/imported/CommerceRaider.png-bf8b21fe06d47d08c44cca1f8235510c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/CommerceRaider.png"
+dest_files=["res://.godot/imported/CommerceRaider.png-bf8b21fe06d47d08c44cca1f8235510c.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Cruiser.png.import b/game/art/units/Cruiser.png.import
new file mode 100644
index 0000000..f66a5ef
--- /dev/null
+++ b/game/art/units/Cruiser.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ux2c2o7mdw62"
+path="res://.godot/imported/Cruiser.png-1e3544dd1a241ffcae8270610e5bfe3d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Cruiser.png"
+dest_files=["res://.godot/imported/Cruiser.png-1e3544dd1a241ffcae8270610e5bfe3d.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Cuirassier.png.import b/game/art/units/Cuirassier.png.import
new file mode 100644
index 0000000..aa7a083
--- /dev/null
+++ b/game/art/units/Cuirassier.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://b1osxxowutu4m"
+path="res://.godot/imported/Cuirassier.png-81579aef297026e6ff19205d26583582.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Cuirassier.png"
+dest_files=["res://.godot/imported/Cuirassier.png-81579aef297026e6ff19205d26583582.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Dragoon.png.import b/game/art/units/Dragoon.png.import
new file mode 100644
index 0000000..9d2cd04
--- /dev/null
+++ b/game/art/units/Dragoon.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cibsp68s8qaiu"
+path="res://.godot/imported/Dragoon.png-54b62cf8d20f1dfb39bb19d654a4dc80.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Dragoon.png"
+dest_files=["res://.godot/imported/Dragoon.png-54b62cf8d20f1dfb39bb19d654a4dc80.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Dreadnought.png.import b/game/art/units/Dreadnought.png.import
new file mode 100644
index 0000000..3753e7b
--- /dev/null
+++ b/game/art/units/Dreadnought.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://380ikkcmhbtq"
+path="res://.godot/imported/Dreadnought.png-aa8d52ce02a2bb43d1f13134ff4a3806.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Dreadnought.png"
+dest_files=["res://.godot/imported/Dreadnought.png-aa8d52ce02a2bb43d1f13134ff4a3806.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Engineer.png.import b/game/art/units/Engineer.png.import
new file mode 100644
index 0000000..9cf2a5c
--- /dev/null
+++ b/game/art/units/Engineer.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ctaadlfgu8gsi"
+path="res://.godot/imported/Engineer.png-f54fdaee08f72f0db7cae32666203020.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Engineer.png"
+dest_files=["res://.godot/imported/Engineer.png-f54fdaee08f72f0db7cae32666203020.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Frigate.png.import b/game/art/units/Frigate.png.import
new file mode 100644
index 0000000..58cbad4
--- /dev/null
+++ b/game/art/units/Frigate.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cayh7gvg3rt2o"
+path="res://.godot/imported/Frigate.png-22a37dd538d573dff9d19359d304af1a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Frigate.png"
+dest_files=["res://.godot/imported/Frigate.png-22a37dd538d573dff9d19359d304af1a.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Guard.png.import b/game/art/units/Guard.png.import
new file mode 100644
index 0000000..859533b
--- /dev/null
+++ b/game/art/units/Guard.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bcwqdjag2ebjl"
+path="res://.godot/imported/Guard.png-e33328551a8b0c631b2c45a2eacf7946.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Guard.png"
+dest_files=["res://.godot/imported/Guard.png-e33328551a8b0c631b2c45a2eacf7946.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Hussar.png.import b/game/art/units/Hussar.png.import
new file mode 100644
index 0000000..b9ee901
--- /dev/null
+++ b/game/art/units/Hussar.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://1k07hjkw32yw"
+path="res://.godot/imported/Hussar.png-f1c1708f6e6fc49464755bd5990c1772.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Hussar.png"
+dest_files=["res://.godot/imported/Hussar.png-f1c1708f6e6fc49464755bd5990c1772.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Infantry.png.import b/game/art/units/Infantry.png.import
new file mode 100644
index 0000000..e971024
--- /dev/null
+++ b/game/art/units/Infantry.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://djapkkohviqg8"
+path="res://.godot/imported/Infantry.png-2f36f0119ae261a77a92edb77e583aa5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Infantry.png"
+dest_files=["res://.godot/imported/Infantry.png-2f36f0119ae261a77a92edb77e583aa5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Ironclad.png.import b/game/art/units/Ironclad.png.import
new file mode 100644
index 0000000..c21f9b5
--- /dev/null
+++ b/game/art/units/Ironclad.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d2xvrhdb8ddbj"
+path="res://.godot/imported/Ironclad.png-6ff432ba040c970e7cf5a34fa1691703.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Ironclad.png"
+dest_files=["res://.godot/imported/Ironclad.png-6ff432ba040c970e7cf5a34fa1691703.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Irregular.png.import b/game/art/units/Irregular.png.import
new file mode 100644
index 0000000..c5afc1f
--- /dev/null
+++ b/game/art/units/Irregular.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://k85dqi6f5h5b"
+path="res://.godot/imported/Irregular.png-3def289eafc49bf0792a13dcfc57644e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Irregular.png"
+dest_files=["res://.godot/imported/Irregular.png-3def289eafc49bf0792a13dcfc57644e.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/ManOWar.png.import b/game/art/units/ManOWar.png.import
new file mode 100644
index 0000000..95e8aae
--- /dev/null
+++ b/game/art/units/ManOWar.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bhs14st1sfxyk"
+path="res://.godot/imported/ManOWar.png-1d0aeaf29ddb7549ac6e1fe02bb71087.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/ManOWar.png"
+dest_files=["res://.godot/imported/ManOWar.png-1d0aeaf29ddb7549ac6e1fe02bb71087.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/Monitor.png.import b/game/art/units/Monitor.png.import
new file mode 100644
index 0000000..abce7fd
--- /dev/null
+++ b/game/art/units/Monitor.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dbr4cvn768avu"
+path="res://.godot/imported/Monitor.png-67ac2d89ae01044a05ecd8ac7a4404e5.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/Monitor.png"
+dest_files=["res://.godot/imported/Monitor.png-67ac2d89ae01044a05ecd8ac7a4404e5.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/art/units/SteamerTransport.png.import b/game/art/units/SteamerTransport.png.import
new file mode 100644
index 0000000..4eece41
--- /dev/null
+++ b/game/art/units/SteamerTransport.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://csusa1o3uhn3b"
+path="res://.godot/imported/SteamerTransport.png-a0d47379c05f4b7a05fb7e1f159d0614.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://art/units/SteamerTransport.png"
+dest_files=["res://.godot/imported/SteamerTransport.png-a0d47379c05f4b7a05fb7e1f159d0614.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/common/map/provinces.json b/game/common/map/provinces.json
new file mode 100644
index 0000000..637a687
--- /dev/null
+++ b/game/common/map/provinces.json
@@ -0,0 +1,238 @@
+{
+ "prov_britain": [150, 0, 0],
+ "prov_ireland": [23, 147, 31],
+ "prov_iceland": "343D91",
+ "prov_corsica": "FFD800",
+ "prov_sardinia": "00FFFF",
+ "prov_sicily": "FFA13D",
+ "prov_malta": "96E6FF",
+ "prov_cyprus": "FFE682",
+ "prov_greenland": "7A66FF",
+ "prov_baleares": "FF9E21",
+ "prov_gotland": "8300FF",
+ "prov_crete": "FF3523",
+ "prov_jan_mayen": "003FFF",
+ "prov_faroes": "964B00",
+ "prov_mann": "935575",
+ "prov_shetlands": "916F7D",
+ "prov_orkney": "8E1C70",
+ "prov_channel_islands": "8C6267",
+ "prov_madiera": "87580C",
+ "prov_canarias": "843740",
+ "prov_sjaeland": "820B02",
+ "prov_bornholm": "7F512F",
+ "prov_bear_island": "AE2AD6",
+ "prov_pantelleria": "FFA575",
+ "prov_iberia": "FF425D",
+ "prov_scandinavia": "005F32",
+ "prov_jutland": "B7004B",
+ "prov_fyn": "B55C63",
+ "prov_hiiumaa": "52FFB2",
+ "prov_saaremaa": "B5E31D",
+ "prov_italy": "47872C",
+ "prov_elba": "333333",
+ "prov_france": "0B0BC6",
+ "prov_netherlands": "FF472C",
+ "prov_belgium": "8C6A16",
+ "prov_luxembourg": "8FB7C6",
+ "prov_switzerland": "A50000",
+ "prov_balkans": "00FF5D",
+ "prov_crimea": "752179",
+ "prov_germany": "303030",
+ "prov_poland": "C90043",
+ "prov_baltics": "6327C4",
+ "prov_finland": "E8E8E8",
+ "prov_aland": "8769B3",
+ "prov_ukraine": "C1CB00",
+ "prov_russia": "006311",
+
+ "prov_north_america": "A114FF",
+
+ "prov_victoria": "59FF93",
+ "prov_calgary": "EA00C7",
+ "prov_saskatoon": "4F8E0F",
+ "prov_winnipeg": "EA4F12",
+ "prov_ottawa": "FF0000",
+ "prov_quebec": "6430AD",
+ "prov_new_brunswick": "53AF3B",
+ "prov_prince_edward_island": "BF4E11",
+ "prov_nova_scotia": "BC0F34",
+ "prov_labrador": "FF2D5E",
+ "prov_newfoundland": "1115FF",
+ "prov_whitehorse": "256B2D",
+ "prov_yellowknife": "A0133B",
+ "prov_iqaluit": "C1AE00",
+
+ "prov_cuba": "1E29FF",
+ "prov_bermuda": "FF2370",
+ "prov_jamaica": "329600",
+ "prov_hispaniola": "742B7C",
+ "prov_aleutians": "FF07AC",
+ "prov_bahamas": "824261",
+ "prov_turks_and_caicos": "7F4424",
+ "prov_puerto_rico": "0D7C57",
+ "prov_barbados": "747A48",
+ "prov_grenada": "32774B",
+ "prov_st_vincent": "535C75",
+ "prov_st_lucia": "E15CEE",
+ "prov_martinique": "EDB274",
+ "prov_dominica": "EAE77E",
+ "prov_guadeloupe": "E8279E",
+ "prov_montserrat": "80AAE5",
+ "prov_antigua": "E2AEB9",
+ "prov_barbuda": "E0966D",
+ "prov_st_kitts": "DD85DA",
+ "prov_west_virgin_islands": "304716",
+ "prov_east_virgin_islands": "44321E",
+ "prov_cayman_islands": "FF7900",
+ "prov_angulia": "FF3D57",
+ "prov_central_america": "005B5E",
+ "prov_mexico": "3BA533",
+
+ "prov_south_america": "2F7A22",
+ "prov_galapagos": "638293",
+ "prov_falklands": "91451F",
+ "prov_south_georgia": "8E8B8E",
+
+ "prov_africa": "7F3300",
+ "prov_madagascar": "790091",
+ "prov_socotra": "89801A",
+ "prov_mauritius": "1C8400",
+ "prov_reunion": "7D821F",
+ "prov_comoros": "7F5731",
+ "prov_sao_tome": "FFAFE8",
+ "prov_fernando_po": "FF665B",
+ "prov_cape_verde": "00FF48",
+ "prov_ascension": "7C3B3E",
+ "prov_st_helena": "7A6C48",
+ "prov_tristan_da_cunha": "77680E",
+ "prov_seychelles": "EF68FF",
+ "prov_prince_edward_islands": "826F68",
+ "prov_kerguelen_islands": "671F7F",
+ "prov_heard_island": "7C4244",
+ "prov_egypt": "A5815B",
+ "prov_morocco": "D44509",
+
+ "prov_middle_east": "615EA8",
+ "prov_ceylon": "FF6A00",
+ "prov_formosa": "82B1FF",
+ "prov_sakhalin": "006421",
+ "prov_maldives": "DC0000",
+ "prov_hainan": "688785",
+ "prov_hokkaido": "BA0DB5",
+ "prov_diego_garcia": "752268",
+ "prov_philippines": "FFA5AD",
+ "prov_india": "720041",
+ "prov_andamans": "706054",
+ "prov_nicobar_islands": "30006B",
+ "prov_indochina": "007272",
+ "prov_korea": "1E2570",
+ "prov_okinawa": "216D38",
+ "prov_yaeyama": "6B120A",
+ "prov_kyushu": "9E2521",
+ "prov_shikoku": "6F9B61",
+ "prov_japan": "99581B",
+ "prov_kurils": "992B15",
+ "prov_manchuria": "B79900",
+ "prov_china": "B57A53",
+ "prov_central_asia": "00B2B2",
+ "prov_siberia": "000EAF",
+ "prov_iran": "703E1D",
+ "prov_anatolia": "A81000",
+
+ "prov_oceania": "FF4C00",
+ "prov_indonesia": "A56A9E",
+ "prov_north_island": "DB41AF",
+ "prov_south_island": "D86E47",
+ "prov_tasmania": "D6BF2C",
+ "prov_australia": "5BB1D3",
+ "prov_hawaii": "008C5D",
+
+ "prov_aral_sea": "AA61C6",
+ "prov_caspian_sea": "00C413",
+ "prov_lake_ladoga": "462A64",
+ "prov_lake_ontario": "46CD64",
+ "prov_lake_erie": "9F2663",
+ "prov_lake_huron": "9FC6EA",
+ "prov_lake_michigan": "9FC62A",
+ "prov_lake_superior": "9F352A",
+ "prov_lake_baikal": "B51453",
+ "prov_lake_woods": "9F892A",
+ "prov_lake_manitoba": "FF0071",
+ "prov_reindeer_lake": "FF7DFF",
+ "prov_lake_ronge": "D9FF51",
+ "prov_lake_athabasca": "FF7D3D",
+ "prov_great_slave_lake": "49FF6B",
+ "prov_great_bear_lake": "FFC6C9",
+
+ "prov_azov_sea": "3E33FF",
+ "prov_black_sea": "3EE1FF",
+ "prov_marmara_sea": "00009D",
+ "prov_agean_sea": "0019FF",
+ "prov_ionian_sea": "003A9D",
+ "prov_adriatic_sea": "00E3FF",
+ "prov_tyrrhenian_sea": "0935FF",
+ "prov_east_mediterranean": "00BBFF",
+ "prov_central_mediterranean": "3A5FDC",
+ "prov_west_mediterranean": "93C5FF",
+ "prov_ligurian_sea": "0000B0",
+ "prov_balearic_sea": "001BFF",
+ "prov_alboran_sea": "3E98FF",
+ "prov_gulf_bothnia": "52E39D",
+ "prov_gulf_finland": "0935C2",
+ "prov_gulf_riga": "004987",
+ "prov_baltic_sea": "5BC5CB",
+ "prov_danish_straits": "004DB0",
+ "prov_english_channel": "0079FF",
+ "prov_irish_sea": "544DFF",
+ "prov_biscay_bay": "93EEFF",
+ "prov_north_sea": "31BBFF",
+ "prov_red_sea": "0952FF",
+ "prov_arabian_sea": "00007E",
+ "prov_persian_gulf": "3EA3FF",
+ "prov_andaman_sea": "0092FF",
+ "prov_bay_bengal": "4075FF",
+ "prov_okhotsk_sea": "66C5FF",
+ "prov_japan_sea": "3A92DC",
+ "prov_east_china_sea": "0066CB",
+ "prov_south_china_sea": "3EEEFF",
+ "prov_philippine_sea": "210066",
+ "prov_celebes_sea": "0066BB",
+ "prov_java_sea": "3E33C9",
+ "prov_banda_sea": "73A3FF",
+ "prov_arafura_sea": "0000D4",
+ "prov_gulf_mexico": "00639D",
+ "prov_caribbean_sea": "00C2DA",
+ "prov_mozambique_channel": "098FFF",
+ "prov_zanj_sea": "93B5FF",
+ "prov_kara_sea": "49FFFF",
+ "prov_barents_sea": "3A40DC",
+ "prov_norwegian_sea": "000087",
+ "prov_greenland_sea": "33EEFF",
+ "prov_labrador_sea": "003787",
+ "prov_hudson_bay": "93F7FF",
+ "prov_gulf_st_lawrence": "4DF7FF",
+ "prov_gulf_alaska": "2C2F87",
+ "prov_gulf_california": "2C2FCF",
+ "prov_east_siberian_sea": "52E3FF",
+ "prov_sargasso_sea": "0019DA",
+ "prov_gulf_guinea": "008BDA",
+ "prov_celtic_sea": "0050B0",
+ "prov_argentine_sea": "0082B0",
+ "prov_chilean_sea": "47FFFF",
+ "prov_north_atlantic": "00B3FF",
+ "prov_central_atlantic": "68C5FF",
+ "prov_south_atlantic": "004DDA",
+ "prov_indian_ocean": "000066",
+ "prov_great_australian_bight": "3A75FF",
+ "prov_tasman_sea": "002CD4",
+ "prov_coral_sea": "8919FF",
+ "prov_melanesia": "3AA8FF",
+ "prov_micronesia": "0098CB",
+ "prov_polynesia": "003EDA",
+ "prov_north_pacific": "00EECF",
+ "prov_south_pacific": "03B3FF",
+ "prov_bering_sea": "0049DA",
+ "prov_chukchi_sea": "0087A5",
+ "prov_beaufort_sea": "93BEFF"
+}
diff --git a/game/common/map/provinces.png b/game/common/map/provinces.png
new file mode 100644
index 0000000..be5b52e
--- /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/regions.json b/game/common/map/regions.json
new file mode 100644
index 0000000..5bb67e2
--- /dev/null
+++ b/game/common/map/regions.json
@@ -0,0 +1,13 @@
+{
+ "region_europe": ["prov_britain", "prov_ireland", "prov_iceland", "prov_corsica", "prov_sardinia", "prov_sicily", "prov_malta", "prov_cyprus", "prov_greenland", "prov_baleares", "prov_gotland", "prov_crete", "prov_jan_mayen", "prov_faroes", "prov_mann", "prov_shetlands", "prov_orkney", "prov_channel_islands", "prov_madiera", "prov_canarias", "prov_sjaeland", "prov_bornholm", "prov_bear_island", "prov_pantelleria", "prov_iberia", "prov_scandinavia", "prov_jutland", "prov_fyn", "prov_hiiumaa", "prov_saaremaa", "prov_italy", "prov_elba", "prov_france", "prov_netherlands", "prov_belgium", "prov_luxembourg", "prov_switzerland", "prov_balkans", "prov_crimea", "prov_germany", "prov_poland", "prov_baltics", "prov_finland", "prov_aland", "prov_ukraine", "prov_russia"],
+
+ "region_north_america": ["prov_north_america", "prov_cuba", "prov_bermuda", "prov_jamaica", "prov_hispaniola", "prov_aleutians", "prov_bahamas", "prov_turks_and_caicos", "prov_puerto_rico", "prov_barbados", "prov_grenada", "prov_st_vincent", "prov_st_lucia", "prov_martinique", "prov_dominica", "prov_guadeloupe", "prov_montserrat", "prov_antigua", "prov_barbuda", "prov_st_kitts", "prov_west_virgin_islands", "prov_east_virgin_islands", "prov_cayman_islands", "prov_angulia", "prov_central_america", "prov_mexico", "prov_victoria", "prov_calgary", "prov_saskatoon", "prov_winnipeg", "prov_ottawa", "prov_quebec", "prov_new_brunswick", "prov_prince_edward_island", "prov_nova_scotia", "prov_labrador", "prov_newfoundland", "prov_whitehorse", "prov_yellowknife", "prov_iqaluit"],
+
+ "region_south_america": ["prov_south_america", "prov_galapagos", "prov_falklands", "prov_south_georgia"],
+
+ "region_africa": ["prov_africa", "prov_madagascar", "prov_socotra", "prov_mauritius", "prov_reunion", "prov_comoros", "prov_sao_tome", "prov_fernando_po", "prov_cape_verde", "prov_ascension", "prov_st_helena", "prov_tristan_da_cunha", "prov_seychelles", "prov_prince_edward_islands", "prov_kerguelen_islands", "prov_heard_island", "prov_egypt", "prov_morocco"],
+
+ "region_asia": ["prov_middle_east", "prov_ceylon", "prov_formosa", "prov_sakhalin", "prov_maldives", "prov_hainan", "prov_hokkaido", "prov_diego_garcia", "prov_philippines", "prov_india", "prov_andamans", "prov_nicobar_islands", "prov_indochina", "prov_korea", "prov_okinawa", "prov_yaeyama", "prov_kyushu", "prov_shikoku", "prov_japan", "prov_kurils", "prov_manchuria", "prov_china", "prov_central_asia", "prov_siberia", "prov_iran", "prov_anatolia"],
+
+ "region_oceania": ["prov_indonesia", "prov_north_island", "prov_south_island", "prov_tasmania", "prov_australia", "prov_hawaii", "prov_oceania"]
+}
diff --git a/game/common/map/water.json b/game/common/map/water.json
new file mode 100644
index 0000000..fda53a9
--- /dev/null
+++ b/game/common/map/water.json
@@ -0,0 +1,6 @@
+
+[
+ "prov_aral_sea", "prov_caspian_sea", "prov_lake_ladoga", "prov_lake_ontario", "prov_lake_erie", "prov_lake_huron", "prov_lake_michigan", "prov_lake_superior", "prov_lake_baikal", "prov_lake_woods", "prov_lake_manitoba", "prov_reindeer_lake", "prov_lake_ronge", "prov_lake_athabasca", "prov_great_slave_lake", "prov_great_bear_lake",
+
+ "prov_azov_sea", "prov_black_sea", "prov_marmara_sea", "prov_agean_sea", "prov_ionian_sea", "prov_adriatic_sea", "prov_tyrrhenian_sea", "prov_east_mediterranean", "prov_central_mediterranean", "prov_west_mediterranean", "prov_ligurian_sea", "prov_balearic_sea", "prov_alboran_sea", "prov_gulf_bothnia", "prov_gulf_finland", "prov_gulf_riga", "prov_baltic_sea", "prov_danish_straits", "prov_english_channel", "prov_irish_sea", "prov_biscay_bay", "prov_north_sea", "prov_red_sea", "prov_arabian_sea", "prov_persian_gulf", "prov_andaman_sea", "prov_bay_bengal", "prov_okhotsk_sea", "prov_japan_sea", "prov_east_china_sea", "prov_south_china_sea", "prov_philippine_sea", "prov_celebes_sea", "prov_java_sea", "prov_banda_sea", "prov_arafura_sea", "prov_gulf_mexico", "prov_caribbean_sea", "prov_mozambique_channel", "prov_zanj_sea", "prov_kara_sea", "prov_barents_sea", "prov_norwegian_sea", "prov_greenland_sea", "prov_labrador_sea", "prov_hudson_bay", "prov_gulf_st_lawrence", "prov_gulf_alaska", "prov_gulf_california", "prov_east_siberian_sea", "prov_sargasso_sea", "prov_gulf_guinea", "prov_celtic_sea", "prov_argentine_sea", "prov_chilean_sea", "prov_north_atlantic", "prov_central_atlantic", "prov_south_atlantic", "prov_indian_ocean", "prov_great_australian_bight", "prov_tasman_sea", "prov_coral_sea", "prov_melanesia", "prov_micronesia", "prov_polynesia", "prov_north_pacific", "prov_south_pacific", "prov_bering_sea", "prov_chukchi_sea", "prov_beaufort_sea"
+]
diff --git a/game/localisation/en_GB/mapmodes.csv b/game/localisation/en_GB/mapmodes.csv
new file mode 100644
index 0000000..440175f
--- /dev/null
+++ b/game/localisation/en_GB/mapmodes.csv
@@ -0,0 +1,6 @@
+
+,, Test Mapmodes
+mapmode_province,Province Mapmode
+mapmode_region,Region Mapmode
+mapmode_terrain,Terrain Mapmode
+mapmode_index,Index Mapmode
diff --git a/game/localisation/en_GB/mapmodes.csv.import b/game/localisation/en_GB/mapmodes.csv.import
new file mode 100644
index 0000000..8dd0c09
--- /dev/null
+++ b/game/localisation/en_GB/mapmodes.csv.import
@@ -0,0 +1,3 @@
+[remap]
+
+importer="keep"
diff --git a/game/localisation/en_GB/menus.csv b/game/localisation/en_GB/menus.csv
index 27a0858..3c3fff1 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
@@ -48,5 +49,29 @@ GAMELOBBY_START,Start Game
GAMELOBBY_BACK,Back
,, Game Session Menu
-GAMESESSIONMENU_RESIGN,Resign
+GAMESESSIONMENU_SAVE,Save Game
+GAMESESSIONMENU_LOAD,Load Game
+GAMESESSIONMENU_OPTIONS,Options
+GAMESESSIONMENU_MAINMENU,Resign
+GAMESESSIONMENU_QUIT,Quit
GAMESESSIONMENU_CLOSE,Close
+
+GAMESESSIONMENU_MAINMENU_DIALOG_TITLE,Resign Game
+GAMESESSIONMENU_MAINMENU_DIALOG_TEXT,Are you sure you want to resign and return to the main menu?
+GAMESESSIONMENU_QUIT_DIALOG_TITLE,Quit Game
+GAMESESSIONMENU_QUIT_DIALOG_TEXT,Are you sure you want to quit and return to desktop?
+
+DIALOG_OK,OK
+DIALOG_CANCEL,Cancel
+DIALOG_SAVE_AND_RESIGN,Save and Resign
+DIALOG_SAVE_AND_QUIT,Save and Quit
+
+,, Province Overview Panel
+province_MISSING,No Province
+region_MISSING,No Region
+LIFE_RATING_TOOLTIP,Liferating: %d
+building_MISSING,No Building
+building_fort,Fort
+building_naval_base,Naval Base
+building_railroad,Railroad
+EXPAND_PROVINCE_BUILDING,Expand
diff --git a/game/localisation/en_GB/provinces.csv b/game/localisation/en_GB/provinces.csv
new file mode 100644
index 0000000..8f1679d
--- /dev/null
+++ b/game/localisation/en_GB/provinces.csv
@@ -0,0 +1,245 @@
+
+,, Europe
+prov_britain,Britain
+prov_ireland,Ireland
+prov_iceland,Iceland
+prov_corsica,Corsica
+prov_sardinia,Sardinia
+prov_sicily,Sicily
+prov_malta,Malta
+prov_cyprus,Cyprus
+prov_greenland,Greenland
+prov_baleares,Balearic Islands
+prov_gotland,Gotland
+prov_crete,Crete
+prov_jan_mayen,Jan Mayen
+prov_faroes,Faroe Islands
+prov_mann,Isle of Mann
+prov_shetlands,Shetlands
+prov_orkney,Orkney
+prov_channel_islands,Channel Islands
+prov_madiera,Madiera
+prov_canarias,Canarias
+prov_sjaeland,Sjaeland
+prov_bornholm,Bornholm
+prov_bear_island,Bear Island
+prov_pantelleria,Pantelleria
+prov_iberia,Iberia
+prov_scandinavia,Scandinavia
+prov_jutland,Jutland
+prov_fyn,Fyn
+prov_hiiumaa,Hiiumaa
+prov_saaremaa,Saaremaa
+prov_italy,Italy
+prov_elba,Elba
+prov_france,France
+prov_netherlands,Netherlands
+prov_belgium,Belgium
+prov_luxembourg,Luxembourg
+prov_switzerland,Switzerland
+prov_balkans,Balkans
+prov_crimea,Crimea
+prov_germany,Germany
+prov_poland,Poland
+prov_baltics,Baltics
+prov_finland,Finland
+prov_aland,Åland
+prov_ukraine,Ukraine
+prov_russia,Russia
+
+,, North America
+prov_north_america_NAME,North America
+prov_cuba_NAME,Cuba
+prov_bermuda_NAME,Bermuda
+prov_jamaica_NAME,Jamaica
+prov_hispaniola_NAME,Hispaniola
+prov_aleutians_name,Aleutian Islands
+prov_bahamas_NAME,Bahamas
+prov_turks_and_caicos_NAME,Turks and Caicos
+prov_puerto_rico_NAME,Puerto Rico
+prov_barbados_NAME,Barbados
+prov_grenada_NAME,Grenada
+prov_st_vincent_NAME,St Vincent
+prov_st_lucia_NAME,St Lucia
+prov_martinique_NAME,Martinique
+prov_dominica_NAME,Dominica
+prov_guadeloupe_NAME,Guadeloupe
+prov_montserrat_NAME,Montserrat
+prov_antigua_NAME,Antigua
+prov_barbuda_NAME,Barbuda
+prov_st_kitts_NAME,St Kitts
+prov_west_virgin_islands_NAME,West Virgin Islands
+prov_east_virgin_islands_NAME,East Virgin Islands
+prov_cayman_islands_NAME,Cayman Islands
+prov_angulia_NAME,Angulia
+prov_central_america_NAME,Central America
+prov_mexico_NAME,Mexico
+
+,, Canada
+prov_victoria_NAME,Victoria
+prov_calgary_NAME,Calgary
+prov_saskatoon_NAME,Saskatoon
+prov_winnipeg_NAME,Winnipeg
+prov_ottawa_NAME,Ottawa
+prov_quebec_NAME,Quebec
+prov_new_brunswick_NAME,New Brunswick
+prov_prince_edward_island_NAME,Prince Edward Island
+prov_nova_scotia_NAME,Nova Scotia
+prov_labrador_NAME,Labrador
+prov_newfoundland_NAME,Newfoundland
+prov_whitehorse_NAME,Whitehorse
+prov_yellowknife_NAME,Yellowknife
+prov_iqaluit_NAME,Iqaluit
+
+,, South America
+prov_south_america,South America
+prov_galapagos,Galapagos
+prov_falklands,Falklands
+prov_south_georgia,South Georgia
+
+,, Africa
+prov_africa,Africa
+prov_madagascar,Madagascar
+prov_socotra,Socotra
+prov_mauritius,Mauritius
+prov_reunion,Réunion
+prov_comoros,Comoros
+prov_sao_tome,Sao Tome
+prov_fernando_po,Fernando Po
+prov_cape_verde,Cape Verde
+prov_ascension,Ascension
+prov_st_helena,St Helena
+prov_tristan_da_cunha,Tristan da Cunha
+prov_seychelles,Seychelles
+prov_prince_edward_islands,Prince Edward Islands
+prov_kerguelen_islands,Kerguelen Islands
+prov_heard_island,Heard Island
+prov_egypt,Egypt
+prov_morocco,Morocco
+
+,, Asia
+prov_middle_east,Middle East
+prov_ceylon,Ceylon
+prov_formosa,Formosa
+prov_sakhalin,Sakhalin
+prov_maldives,Maldives
+prov_hainan,Hainan
+prov_hokkaido,Hokkaido
+prov_diego_garcia,Diego Garcia
+prov_philippines,Philippines
+prov_india,India
+prov_andamans,Andaman Islands
+prov_nicobar_islands,Nicobar Islands
+prov_indochina,Indochina
+prov_korea,Korea
+prov_okinawa,Okinawa
+prov_yaeyama,Yaeyama
+prov_kyushu,Kyushu
+prov_shikoku,Shikoku
+prov_japan,Japan
+prov_kurils,Kuril Islands
+prov_manchuria,Manchuria
+prov_china,China
+prov_central_asia,Central Asia
+prov_siberia,Siberia
+prov_iran,Iran
+prov_anatolia,Anatolia
+
+,, Oceania
+prov_oceania,Oceania
+prov_indonesia,Indonesia
+prov_north_island,North Island
+prov_south_island,South Island
+prov_tasmania,Tasmania
+prov_australia,Australia
+prov_hawaii,Hawaii
+
+,, Lakes
+prov_aral_sea,Aral Sea
+prov_caspian_sea,Caspian Sea
+prov_lake_ladoga,Lake Ladoga
+prov_lake_ontario,Lake Ontario
+prov_lake_erie,Lake Erie
+prov_lake_huron,Lake Huron
+prov_lake_michigan,Lake Michigan
+prov_lake_superior,Lake Superior
+prov_lake_baikal,Lake Baikal
+prov_lake_woods,Lake of the Woods
+prov_lake_manitoba,Lake Manitoba
+prov_reindeer_lake,Reindeer Lake
+prov_lake_ronge,Lac la Ronge
+prov_lake_athabasca,Lake Athabasca
+prov_great_slave_lake,Great Slave Lake
+prov_great_bear_lake,Great Bear Lake
+
+,, Seas and Oceans
+prov_azov_sea,Sea of Azov
+prov_black_sea,Black Sea
+prov_marmara_sea,Sea of Marmara
+prov_agean_sea,Agean Sea
+prov_ionian_sea,Ionian Sea
+prov_adriatic_sea,Adriatic Sea
+prov_tyrrhenian_sea,Tyrrhenian Sea
+prov_east_mediterranean,East Mediterranean Sea
+prov_central_mediterranean,Central Mediterranean Sea
+prov_west_mediterranean,West Mediterranean Sea
+prov_ligurian_sea,Ligurian Sea
+prov_balearic_sea,Balearic Sea
+prov_alboran_sea,Alboran Sea
+prov_gulf_bothnia,Gulf of Bothnia
+prov_gulf_finland,Gulf of Finland
+prov_gulf_riga,Gulf of Riga
+prov_baltic_sea,Baltic Sea
+prov_danish_straits,Danish Straits
+prov_english_channel,English Channel
+prov_irish_sea,Irish Sea
+prov_biscay_bay,Bay of Biscay
+prov_north_sea,North Sea
+prov_red_sea,Red Sea
+prov_arabian_sea,Arabian Sea
+prov_persian_gulf,Persian Gulf
+prov_andaman_sea,Andaman Sea
+prov_bay_bengal,Bay of Bengal
+prov_okhotsk_sea,Sea of Okhotsk
+prov_japan_sea,Sea of Japan
+prov_east_china_sea,East China Sea
+prov_south_china_sea,South China Sea
+prov_philippine_sea,Philippine Sea
+prov_celebes_sea,Celebes Sea
+prov_java_sea,Java Sea
+prov_banda_sea,Banda Sea
+prov_arafura_sea,Arafura Sea
+prov_gulf_mexico,Gulf of Mexico
+prov_caribbean_sea,Caribbean Sea
+prov_mozambique_channel,Mozambique Channel
+prov_zanj_sea,Sea of Zanj
+prov_kara_sea,Kara Sea
+prov_barents_sea,Barents Sea
+prov_norwegian_sea,Norwegian Sea
+prov_greenland_sea,Greenland Sea
+prov_labrador_sea,Labrador Sea
+prov_hudson_bay,Hudson Bay
+prov_gulf_st_lawrence,Gulf of St Lawrence
+prov_gulf_alaska,Gulf of Alaska
+prov_gulf_california,Gulf of California
+prov_east_siberian_sea,East Siberian Sea
+prov_sargasso_sea,Sargasso Sea
+prov_gulf_guinea,Gulf of Guinea
+prov_celtic_sea,Celtic Sea
+prov_argentine_sea,Argentine Sea
+prov_chilean_sea,Chilean Sea
+prov_north_atlantic,North Atlantic Ocean
+prov_central_atlantic,Central Atlantic Ocean
+prov_south_atlantic,South Atlantic Ocean
+prov_indian_ocean,Indian Ocean
+prov_great_australian_bight,Great Australian Bight
+prov_tasman_sea,Tasman Sea
+prov_coral_sea,Coral Sea
+prov_melanesia,Melanesia
+prov_micronesia,Micronesia
+prov_polynesia,Polynesia
+prov_north_pacific,North Pacific Ocean
+prov_south_pacific,South Pacific Ocean
+prov_bering_sea,Bering Sea
+prov_chukchi_sea,Chukchi Sea
+prov_beaufort_sea,Beaufort Sea
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/localisation/en_GB/regions.csv b/game/localisation/en_GB/regions.csv
new file mode 100644
index 0000000..258cce6
--- /dev/null
+++ b/game/localisation/en_GB/regions.csv
@@ -0,0 +1,8 @@
+
+,, Regions
+region_europe,Europe
+region_north_america,North America
+region_south_america,South America
+region_africa,Africa
+region_asia,Asia
+region_oceania,Oceania
diff --git a/game/localisation/en_GB/regions.csv.import b/game/localisation/en_GB/regions.csv.import
new file mode 100644
index 0000000..8dd0c09
--- /dev/null
+++ b/game/localisation/en_GB/regions.csv.import
@@ -0,0 +1,3 @@
+[remap]
+
+importer="keep"
diff --git a/game/localisation/en_US/menus.csv b/game/localisation/en_US/menus.csv
index 27a0858..807d1cb 100644
--- a/game/localisation/en_US/menus.csv
+++ b/game/localisation/en_US/menus.csv
@@ -1,3 +1,4 @@
+
,, Main Menu
MAINMENU_TITLE,OpenVic2
MAINMENU_NEW_GAME,New Game
@@ -48,5 +49,9 @@ GAMELOBBY_START,Start Game
GAMELOBBY_BACK,Back
,, Game Session Menu
-GAMESESSIONMENU_RESIGN,Resign
+GAMESESSIONMENU_SAVE,Save Game
+GAMESESSIONMENU_LOAD,Load Game
+GAMESESSIONMENU_OPTIONS,Options
+GAMESESSIONMENU_MAINMENU,Resign
+GAMESESSIONMENU_QUIT,Quit
GAMESESSIONMENU_CLOSE,Close
diff --git a/game/localisation/fr_FR/menus.csv b/game/localisation/fr_FR/menus.csv
index 8e8a7ad..0d84b64 100644
--- a/game/localisation/fr_FR/menus.csv
+++ b/game/localisation/fr_FR/menus.csv
@@ -1,3 +1,4 @@
+
,, Main Menu
MAINMENU_TITLE,OpenVic2
MAINMENU_NEW_GAME,Nouveau Jeu
@@ -48,5 +49,9 @@ GAMELOBBY_START,Démarrer Jeu
GAMELOBBY_BACK,Retourner
,, Game Session Menu
-GAMESESSIONMENU_RESIGN,Démissionner
+GAMESESSIONMENU_SAVE,Sauvegarder la Partie
+GAMESESSIONMENU_LOAD,Charger la Partie
+GAMESESSIONMENU_OPTIONS,Options
+GAMESESSIONMENU_MAINMENU,Démissionner
+GAMESESSIONMENU_QUIT,Quitter
GAMESESSIONMENU_CLOSE,Fermer
diff --git a/game/project.godot b/game/project.godot
index 363bbc1..ff4081e 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_zoom_in={
+"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_zoom_out={
+"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..7540d3e 100644
--- a/game/src/Autoload/Events.gd
+++ b/game/src/Autoload/Events.gd
@@ -2,3 +2,20 @@ 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 _water_province_file : String = "res://common/map/water.json"
+const _region_file : String = "res://common/map/regions.json"
+const _province_shape_file : String = "res://common/map/provinces.png"
+
+# REQUIREMENTS
+# * FS-333, FS-334, FS-335, FS-341
+func _ready():
+ if GameSingleton.load_province_identifier_file(_province_identifier_file) != OK:
+ push_error("Failed to load province identifiers")
+ if GameSingleton.load_water_province_file(_water_province_file) != OK:
+ push_error("Failed to load water provinces")
+ if GameSingleton.load_region_file(_region_file) != OK:
+ push_error("Failed to load regions")
+ if GameSingleton.load_province_shape_file(_province_shape_file) != OK:
+ push_error("Failed to load province shapes")
diff --git a/game/src/GameMenu.tscn b/game/src/GameMenu.tscn
index aabb29c..c642351 100644
--- a/game/src/GameMenu.tscn
+++ b/game/src/GameMenu.tscn
@@ -43,7 +43,6 @@ anchor_left = 1.0
anchor_right = 1.0
offset_left = -184.0
offset_right = -34.0
-offset_bottom = 110.0
grow_horizontal = 0
[connection signal="credits_button_pressed" from="MainMenu" to="." method="_on_main_menu_credits_button_pressed"]
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;
+}
diff --git a/game/src/LobbyMenu/LobbyMenu.gd b/game/src/LobbyMenu/LobbyMenu.gd
index 802dac3..4fc06c9 100644
--- a/game/src/LobbyMenu/LobbyMenu.gd
+++ b/game/src/LobbyMenu/LobbyMenu.gd
@@ -34,3 +34,8 @@ func _on_game_select_list_item_selected(index):
func _on_save_game_selected(_index):
start_button.disabled = false
+
+# If the date is double-clicked, start the game!
+func _on_game_select_list_item_activated(index):
+ _on_game_select_list_item_selected(index)
+ _on_start_button_button_down()
diff --git a/game/src/LobbyMenu/LobbyMenu.tscn b/game/src/LobbyMenu/LobbyMenu.tscn
index 528e7ae..174fb72 100644
--- a/game/src/LobbyMenu/LobbyMenu.tscn
+++ b/game/src/LobbyMenu/LobbyMenu.tscn
@@ -78,6 +78,7 @@ custom_minimum_size = Vector2(0, 33)
layout_mode = 2
[connection signal="save_game_selected" from="." to="." method="_on_save_game_selected"]
+[connection signal="item_activated" from="GameSelectPanel/VBoxContainer/GameSelectList" to="." method="_on_game_select_list_item_activated"]
[connection signal="item_selected" from="GameSelectPanel/VBoxContainer/GameSelectList" to="." method="_on_game_select_list_item_selected"]
[connection signal="button_down" from="GameSelectPanel/VBoxContainer/BackButton" to="." method="_on_back_button_button_down"]
[connection signal="button_down" from="GameStartPanel/VBoxContainer/StartButton" to="." method="_on_start_button_button_down"]
diff --git a/game/src/MainMenu/MainMenu.gd b/game/src/MainMenu/MainMenu.gd
index 4420786..9d0edc6 100644
--- a/game/src/MainMenu/MainMenu.gd
+++ b/game/src/MainMenu/MainMenu.gd
@@ -10,8 +10,6 @@ var _new_game_button : BaseButton
# REQUIREMENTS:
# * SS-3
func _ready():
- print("From GDScript")
- TestSingleton.hello_singleton()
_on_new_game_button_visibility_changed()
# REQUIREMENTS:
diff --git a/game/src/MainMenu/MainMenu.tscn b/game/src/MainMenu/MainMenu.tscn
index 5fb6ca9..0618fe8 100644
--- a/game/src/MainMenu/MainMenu.tscn
+++ b/game/src/MainMenu/MainMenu.tscn
@@ -1,7 +1,8 @@
-[gd_scene load_steps=5 format=3 uid="uid://bp5n3mlu45ygw"]
+[gd_scene load_steps=6 format=3 uid="uid://bp5n3mlu45ygw"]
[ext_resource type="Theme" uid="uid://qoi3oec48jp0" path="res://theme/main_menu.tres" id="1_1yri4"]
[ext_resource type="Script" path="res://src/MainMenu/MainMenu.gd" id="2_nm1fq"]
+[ext_resource type="Texture2D" uid="uid://dxys0wg0f0ic5" path="res://theme/assets/OpenVicFINALREALTRANS.png" id="3_58ess"]
[ext_resource type="PackedScene" uid="uid://b7oncobnacxmt" path="res://src/LocaleButton.tscn" id="3_amonp"]
[ext_resource type="PackedScene" uid="uid://cen7wkmn6og66" path="res://src/MainMenu/ReleaseInfoBox.tscn" id="3_km0er"]
@@ -15,9 +16,9 @@ grow_horizontal = 2
grow_vertical = 2
theme = ExtResource("1_1yri4")
script = ExtResource("2_nm1fq")
-_new_game_button = NodePath("Panel/VBox/Margin/ButtonList/NewGameButton")
+_new_game_button = NodePath("MenuPanel/MenuList/ButtonListMargin/ButtonList/NewGameButton")
-[node name="Panel" type="PanelContainer" parent="."]
+[node name="MenuPanel" type="PanelContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
@@ -26,31 +27,30 @@ grow_horizontal = 2
grow_vertical = 2
theme_type_variation = &"BackgroundPanel"
-[node name="VBox" type="VBoxContainer" parent="Panel"]
+[node name="MenuList" type="VBoxContainer" parent="MenuPanel"]
layout_mode = 2
-[node name="TitleLabel" type="Label" parent="Panel/VBox"]
+[node name="TitleIcon" type="TextureRect" parent="MenuPanel/MenuList"]
layout_mode = 2
-size_flags_vertical = 6
-size_flags_stretch_ratio = 1.5
-theme_type_variation = &"TitleLabel"
-text = "MAINMENU_TITLE"
-horizontal_alignment = 1
-vertical_alignment = 1
-
-[node name="Margin" type="MarginContainer" parent="Panel/VBox"]
+size_flags_vertical = 3
+size_flags_stretch_ratio = 1.75
+texture = ExtResource("3_58ess")
+expand_mode = 1
+stretch_mode = 5
+
+[node name="ButtonListMargin" type="MarginContainer" parent="MenuPanel/MenuList"]
layout_mode = 2
theme_override_constants/margin_left = 15
theme_override_constants/margin_right = 12
-[node name="ButtonList" type="HBoxContainer" parent="Panel/VBox/Margin"]
+[node name="ButtonList" type="HBoxContainer" parent="MenuPanel/MenuList/ButtonListMargin"]
custom_minimum_size = Vector2(500, 0)
layout_mode = 2
theme_type_variation = &"HBox_MainMenu_ButtonList"
theme_override_constants/separation = 18
alignment = 1
-[node name="NewGameButton" type="Button" parent="Panel/VBox/Margin/ButtonList"]
+[node name="NewGameButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
editor_description = "UI-26"
layout_mode = 2
size_flags_horizontal = 3
@@ -63,7 +63,7 @@ theme_type_variation = &"TitleButton"
text = "MAINMENU_NEW_GAME"
clip_text = true
-[node name="ContinueButton" type="Button" parent="Panel/VBox/Margin/ButtonList"]
+[node name="ContinueButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
layout_mode = 2
size_flags_horizontal = 3
focus_neighbor_left = NodePath("../NewGameButton")
@@ -75,7 +75,7 @@ disabled = true
text = "MAINMENU_CONTINUE"
clip_text = true
-[node name="MultiplayerButton" type="Button" parent="Panel/VBox/Margin/ButtonList"]
+[node name="MultiplayerButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
editor_description = "UI-27"
layout_mode = 2
size_flags_horizontal = 3
@@ -87,7 +87,7 @@ theme_type_variation = &"TitleButton"
text = "MAINMENU_MULTIPLAYER"
clip_text = true
-[node name="OptionsButton" type="Button" parent="Panel/VBox/Margin/ButtonList"]
+[node name="OptionsButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
editor_description = "UI-5"
layout_mode = 2
size_flags_horizontal = 3
@@ -99,7 +99,7 @@ theme_type_variation = &"TitleButton"
text = "MAINMENU_OPTIONS"
clip_text = true
-[node name="CreditsButton" type="Button" parent="Panel/VBox/Margin/ButtonList"]
+[node name="CreditsButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
editor_description = "UI-32"
layout_mode = 2
size_flags_horizontal = 3
@@ -111,7 +111,7 @@ theme_type_variation = &"TitleButton"
text = "MAINMENU_CREDITS"
clip_text = true
-[node name="ExitButton" type="Button" parent="Panel/VBox/Margin/ButtonList"]
+[node name="ExitButton" type="Button" parent="MenuPanel/MenuList/ButtonListMargin/ButtonList"]
editor_description = "UI-3"
layout_mode = 2
size_flags_horizontal = 3
@@ -123,28 +123,28 @@ theme_type_variation = &"TitleButton"
text = "MAINMENU_EXIT"
clip_text = true
-[node name="BottomSpace" type="Control" parent="Panel/VBox"]
+[node name="BottomSpace" type="Control" parent="MenuPanel/MenuList"]
layout_mode = 2
size_flags_vertical = 3
size_flags_stretch_ratio = 0.35
-[node name="BottomMargin" type="MarginContainer" parent="Panel/VBox"]
+[node name="BottomMargin" type="MarginContainer" parent="MenuPanel/MenuList"]
layout_mode = 2
theme_type_variation = &"BottomMargin"
-[node name="ReleaseInfoBox" parent="Panel/VBox/BottomMargin" instance=ExtResource("3_km0er")]
+[node name="ReleaseInfoBox" parent="MenuPanel/MenuList/BottomMargin" instance=ExtResource("3_km0er")]
layout_mode = 2
-[node name="LocaleButton" parent="Panel/VBox/BottomMargin" instance=ExtResource("3_amonp")]
+[node name="LocaleButton" parent="MenuPanel/MenuList/BottomMargin" instance=ExtResource("3_amonp")]
layout_mode = 2
size_flags_horizontal = 8
alignment = 0
text_overrun_behavior = 4
-[connection signal="pressed" from="Panel/VBox/Margin/ButtonList/NewGameButton" to="." method="_on_new_game_button_pressed"]
-[connection signal="visibility_changed" from="Panel/VBox/Margin/ButtonList/NewGameButton" to="." method="_on_new_game_button_visibility_changed"]
-[connection signal="pressed" from="Panel/VBox/Margin/ButtonList/ContinueButton" to="." method="_on_continue_button_pressed"]
-[connection signal="pressed" from="Panel/VBox/Margin/ButtonList/MultiplayerButton" to="." method="_on_multi_player_button_pressed"]
-[connection signal="pressed" from="Panel/VBox/Margin/ButtonList/OptionsButton" to="." method="_on_options_button_pressed"]
-[connection signal="pressed" from="Panel/VBox/Margin/ButtonList/CreditsButton" to="." method="_on_credits_button_pressed"]
-[connection signal="pressed" from="Panel/VBox/Margin/ButtonList/ExitButton" to="." method="_on_exit_button_pressed"]
+[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/NewGameButton" to="." method="_on_new_game_button_pressed"]
+[connection signal="visibility_changed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/NewGameButton" to="." method="_on_new_game_button_visibility_changed"]
+[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/ContinueButton" to="." method="_on_continue_button_pressed"]
+[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/MultiplayerButton" to="." method="_on_multi_player_button_pressed"]
+[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/OptionsButton" to="." method="_on_options_button_pressed"]
+[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/CreditsButton" to="." method="_on_credits_button_pressed"]
+[connection signal="pressed" from="MenuPanel/MenuList/ButtonListMargin/ButtonList/ExitButton" to="." method="_on_exit_button_pressed"]
diff --git a/game/src/MusicConductor/MusicConductor.gd b/game/src/MusicConductor/MusicConductor.gd
index c0cfc46..08fa86a 100644
--- a/game/src/MusicConductor/MusicConductor.gd
+++ b/game/src/MusicConductor/MusicConductor.gd
@@ -36,7 +36,7 @@ func is_paused() -> bool:
func toggle_play_pause() -> void:
$AudioStreamPlayer.stream_paused = !$AudioStreamPlayer.stream_paused
-
+
func start_current_song() -> void:
$AudioStreamPlayer.stream = _available_songs[_selected_track].song_stream
$AudioStreamPlayer.play()
diff --git a/game/src/MusicConductor/MusicPlayer.gd b/game/src/MusicConductor/MusicPlayer.gd
index baf8a43..b775b84 100644
--- a/game/src/MusicConductor/MusicPlayer.gd
+++ b/game/src/MusicConductor/MusicPlayer.gd
@@ -44,6 +44,8 @@ func _on_previous_song_button_pressed():
func _on_option_button_item_selected(index):
# UIFUN-92
MusicConductor.start_song_by_index(index)
+ _update_song_name_visual()
+ _update_play_pause_button()
func _on_progress_slider_drag_started():
diff --git a/game/src/MusicConductor/MusicPlayer.tscn b/game/src/MusicConductor/MusicPlayer.tscn
index 91b6fd3..80ad641 100644
--- a/game/src/MusicConductor/MusicPlayer.tscn
+++ b/game/src/MusicConductor/MusicPlayer.tscn
@@ -4,58 +4,60 @@
[node name="MusicPlayer" type="BoxContainer" node_paths=PackedStringArray("_song_selector_button", "_progress_slider", "_previous_song_button", "_play_pause_button", "_next_song_button", "_visbility_button")]
editor_description = "UI-104"
+offset_right = 150.0
+offset_bottom = 110.0
mouse_filter = 2
+vertical = true
script = ExtResource("1_gcm4m")
-_song_selector_button = NodePath("Control/SongSelectorButton")
-_progress_slider = NodePath("Control/ProgressSlider")
-_previous_song_button = NodePath("Control/HBoxContainer/PreviousSongButton")
-_play_pause_button = NodePath("Control/HBoxContainer/PlayPauseButton")
-_next_song_button = NodePath("Control/HBoxContainer/NextSongButton")
-_visbility_button = NodePath("Control/MusicUIVisibilityButton")
-
-[node name="Control" type="VBoxContainer" parent="."]
-custom_minimum_size = Vector2(150, 0)
-layout_mode = 2
-mouse_filter = 2
-
-[node name="SongSelectorButton" type="OptionButton" parent="Control"]
+_song_selector_button = NodePath("SongSelectorButton")
+_progress_slider = NodePath("ProgressSlider")
+_previous_song_button = NodePath("ButtonList/PreviousSongButton")
+_play_pause_button = NodePath("ButtonList/PlayPauseButton")
+_next_song_button = NodePath("ButtonList/NextSongButton")
+_visbility_button = NodePath("MusicUIVisibilityButton")
+
+[node name="SongSelectorButton" type="OptionButton" parent="."]
editor_description = "UI-107"
+custom_minimum_size = Vector2(150, 0)
layout_mode = 2
alignment = 1
text_overrun_behavior = 3
fit_to_longest_item = false
-[node name="ProgressSlider" type="HSlider" parent="Control"]
+[node name="ProgressSlider" type="HSlider" parent="."]
+custom_minimum_size = Vector2(150, 0)
layout_mode = 2
+size_flags_vertical = 1
-[node name="HBoxContainer" type="HBoxContainer" parent="Control"]
+[node name="ButtonList" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 4
mouse_filter = 2
-[node name="PreviousSongButton" type="Button" parent="Control/HBoxContainer"]
+[node name="PreviousSongButton" type="Button" parent="ButtonList"]
layout_mode = 2
text = "<"
-[node name="PlayPauseButton" type="Button" parent="Control/HBoxContainer"]
+[node name="PlayPauseButton" type="Button" parent="ButtonList"]
custom_minimum_size = Vector2(30, 0)
layout_mode = 2
text = "▶"
-[node name="NextSongButton" type="Button" parent="Control/HBoxContainer"]
+[node name="NextSongButton" type="Button" parent="ButtonList"]
layout_mode = 2
text = ">"
-[node name="MusicUIVisibilityButton" type="Button" parent="Control"]
+[node name="MusicUIVisibilityButton" type="Button" parent="."]
editor_description = "UI-106"
layout_mode = 2
size_flags_horizontal = 4
+toggle_mode = true
text = "⬆"
-[connection signal="item_selected" from="Control/SongSelectorButton" to="." method="_on_option_button_item_selected"]
-[connection signal="drag_ended" from="Control/ProgressSlider" to="." method="_on_progress_slider_drag_ended"]
-[connection signal="drag_started" from="Control/ProgressSlider" to="." method="_on_progress_slider_drag_started"]
-[connection signal="pressed" from="Control/HBoxContainer/PreviousSongButton" to="." method="_on_previous_song_button_pressed"]
-[connection signal="pressed" from="Control/HBoxContainer/PlayPauseButton" to="." method="_on_play_pause_button_pressed"]
-[connection signal="pressed" from="Control/HBoxContainer/NextSongButton" to="." method="_on_next_song_button_pressed"]
-[connection signal="pressed" from="Control/MusicUIVisibilityButton" to="." method="_on_music_ui_visibility_button_pressed"]
+[connection signal="item_selected" from="SongSelectorButton" to="." method="_on_option_button_item_selected"]
+[connection signal="drag_ended" from="ProgressSlider" to="." method="_on_progress_slider_drag_ended"]
+[connection signal="drag_started" from="ProgressSlider" to="." method="_on_progress_slider_drag_started"]
+[connection signal="pressed" from="ButtonList/PreviousSongButton" to="." method="_on_previous_song_button_pressed"]
+[connection signal="pressed" from="ButtonList/PlayPauseButton" to="." method="_on_play_pause_button_pressed"]
+[connection signal="pressed" from="ButtonList/NextSongButton" to="." method="_on_next_song_button_pressed"]
+[connection signal="pressed" from="MusicUIVisibilityButton" to="." method="_on_music_ui_visibility_button_pressed"]
diff --git a/game/src/Utility/StyleBoxWithSound.gd b/game/src/Utility/StyleBoxWithSound.gd
index 8de1af1..8c29b34 100644
--- a/game/src/Utility/StyleBoxWithSound.gd
+++ b/game/src/Utility/StyleBoxWithSound.gd
@@ -1,3 +1,4 @@
+## WARNING: This will not work with togglable UI elements, a special implementation is needed for them.
@tool
extends StyleBox
class_name StyleBoxWithSound
diff --git a/game/theme/assets/OpenVicFINALREALTRANS.png b/game/theme/assets/OpenVicFINALREALTRANS.png
new file mode 100644
index 0000000..446814f
--- /dev/null
+++ b/game/theme/assets/OpenVicFINALREALTRANS.png
Binary files differ
diff --git a/game/theme/assets/OpenVicFINALREALTRANS.png.import b/game/theme/assets/OpenVicFINALREALTRANS.png.import
new file mode 100644
index 0000000..6963753
--- /dev/null
+++ b/game/theme/assets/OpenVicFINALREALTRANS.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dxys0wg0f0ic5"
+path="res://.godot/imported/OpenVicFINALREALTRANS.png-be4facd2c0164660117ada12fd221f10.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://theme/assets/OpenVicFINALREALTRANS.png"
+dest_files=["res://.godot/imported/OpenVicFINALREALTRANS.png-be4facd2c0164660117ada12fd221f10.ctex"]
+
+[params]
+
+compress/mode=0
+compress/high_quality=false
+compress/lossy_quality=0.7
+compress/hdr_compression=1
+compress/normal_map=0
+compress/channel_pack=0
+mipmaps/generate=false
+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=1
diff --git a/game/theme/game_session_menu.tres b/game/theme/game_session_menu.tres
new file mode 100644
index 0000000..42775c3
--- /dev/null
+++ b/game/theme/game_session_menu.tres
@@ -0,0 +1,84 @@
+[gd_resource type="Theme" load_steps=11 format=3 uid="uid://dndova5cw036e"]
+
+[ext_resource type="StyleBox" uid="uid://blwilunhmyvpq" path="res://theme/assets/main_menu_button_normal.tres" id="1_7med2"]
+[ext_resource type="Script" path="res://src/Utility/StyleBoxWithSound.gd" id="2_oj3dv"]
+[ext_resource type="AudioStream" uid="uid://bsldcs3l8s7ug" path="res://addons/kenney_ui_audio/click3.wav" id="3_j823n"]
+[ext_resource type="Texture2D" uid="uid://c0p34i3d3b0pw" path="res://theme/assets/main_menu_button.png" id="4_lno5s"]
+
+[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_jvvyi"]
+content_margin_left = 20.0
+content_margin_top = 10.0
+content_margin_right = 20.0
+content_margin_bottom = 14.0
+texture = ExtResource("4_lno5s")
+modulate_color = Color(0.817521, 0.817521, 0.817521, 0.784314)
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_6ab1x"]
+draw_center = false
+border_width_left = 10
+border_width_top = 15
+border_width_right = 10
+border_width_bottom = 15
+border_color = Color(0, 0, 0, 0.584314)
+border_blend = true
+corner_radius_top_left = 5
+corner_radius_top_right = 5
+corner_radius_bottom_right = 5
+corner_radius_bottom_left = 5
+corner_detail = 20
+
+[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_jslj0"]
+content_margin_left = 20.0
+content_margin_top = 10.0
+content_margin_right = 20.0
+content_margin_bottom = 14.0
+texture = ExtResource("4_lno5s")
+modulate_color = Color(0.588235, 0.588235, 0.588235, 1)
+
+[sub_resource type="StyleBoxTexture" id="StyleBoxTexture_l2rw3"]
+content_margin_left = 20.0
+content_margin_top = 10.0
+content_margin_right = 20.0
+content_margin_bottom = 14.0
+texture = ExtResource("4_lno5s")
+modulate_color = Color(0.85098, 0.85098, 0.85098, 1)
+
+[sub_resource type="StyleBox" id="StyleBox_ptkcj"]
+resource_local_to_scene = false
+resource_name = ""
+content_margin_left = -1.0
+content_margin_top = -1.0
+content_margin_right = -1.0
+content_margin_bottom = -1.0
+script = ExtResource("2_oj3dv")
+style_box = SubResource("StyleBoxTexture_l2rw3")
+sound = ExtResource("3_j823n")
+
+[sub_resource type="StyleBoxLine" id="StyleBoxLine_kaw4i"]
+color = Color(0.454902, 0.45098, 0.435294, 1)
+thickness = 2
+
+[resource]
+SessionButton/base_type = &"Button"
+SessionButton/colors/font_color = Color(0.87451, 0.87451, 0.87451, 1)
+SessionButton/colors/font_disabled_color = Color(0.87451, 0.87451, 0.87451, 0.501961)
+SessionButton/colors/font_focus_color = Color(0.94902, 0.94902, 0.94902, 1)
+SessionButton/colors/font_hover_color = Color(0.94902, 0.94902, 0.94902, 1)
+SessionButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
+SessionButton/colors/font_outline_color = Color(1, 1, 1, 1)
+SessionButton/colors/font_pressed_color = Color(1, 1, 1, 1)
+SessionButton/colors/icon_disabled_color = Color(1, 1, 1, 0.4)
+SessionButton/colors/icon_focus_color = Color(0.94902, 0.94902, 0.94902, 1)
+SessionButton/colors/icon_hover_color = Color(0.94902, 0.94902, 0.94902, 1)
+SessionButton/colors/icon_hover_pressed_color = Color(1, 1, 1, 1)
+SessionButton/colors/icon_normal_color = Color(1, 1, 1, 1)
+SessionButton/colors/icon_pressed_color = Color(1, 1, 1, 1)
+SessionButton/styles/disabled = SubResource("StyleBoxTexture_jvvyi")
+SessionButton/styles/focus = SubResource("StyleBoxFlat_6ab1x")
+SessionButton/styles/hover = SubResource("StyleBoxTexture_jslj0")
+SessionButton/styles/normal = ExtResource("1_7med2")
+SessionButton/styles/pressed = SubResource("StyleBox_ptkcj")
+SessionPanel/base_type = &"Panel"
+SessionPanel/styles/panel = null
+SessionSeparator/base_type = &"HSeparator"
+SessionSeparator/styles/separator = SubResource("StyleBoxLine_kaw4i")
diff --git a/game/theme/main_menu.tres b/game/theme/main_menu.tres
index 24c5552..0518cd8 100644
--- a/game/theme/main_menu.tres
+++ b/game/theme/main_menu.tres
@@ -171,9 +171,9 @@ CommitLabel/styles/normal = SubResource("StyleBoxEmpty_kmfi1")
CommitLabel/styles/pressed = SubResource("StyleBoxEmpty_1qcrh")
TitleButton/base_type = &"Button"
TitleButton/colors/font_color = Color(0.87451, 0.87451, 0.87451, 1)
-TitleButton/colors/font_disabled_color = Color(0.875, 0.875, 0.875, 0.5)
-TitleButton/colors/font_focus_color = Color(0.95, 0.95, 0.95, 1)
-TitleButton/colors/font_hover_color = Color(0.95, 0.95, 0.95, 1)
+TitleButton/colors/font_disabled_color = Color(0.87451, 0.87451, 0.87451, 0.501961)
+TitleButton/colors/font_focus_color = Color(0.94902, 0.94902, 0.94902, 1)
+TitleButton/colors/font_hover_color = Color(0.94902, 0.94902, 0.94902, 1)
TitleButton/colors/font_hover_pressed_color = Color(1, 1, 1, 1)
TitleButton/colors/font_outline_color = Color(1, 1, 1, 1)
TitleButton/colors/font_pressed_color = Color(1, 1, 1, 1)
@@ -189,8 +189,6 @@ TitleButton/styles/focus = SubResource("StyleBoxFlat_2txce")
TitleButton/styles/hover = SubResource("StyleBoxTexture_3efxh")
TitleButton/styles/normal = ExtResource("6_dx0aj")
TitleButton/styles/pressed = SubResource("StyleBox_uuspe")
-TitleLabel/base_type = &"Label"
-TitleLabel/font_sizes/font_size = 90
VersionLabel/base_type = &"Button"
VersionLabel/colors/font_hover_color = Color(0.360784, 0.360784, 0.360784, 1)
VersionLabel/colors/font_hover_pressed_color = Color(0.215686, 0.215686, 0.215686, 1)
diff --git a/godot-cpp b/godot-cpp
-Subproject 4d3afc0ad0c5445831418830a33c6adb3e0a9aa
+Subproject 7fb46e9ea1571b1364ab049b2088e9b302ff798