diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..63295c3
Binary files /dev/null and b/.DS_Store differ
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..f28239b
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,4 @@
+root = true
+
+[*]
+charset = utf-8
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..5b0d1a4
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,3 @@
+# Normalize EOL for all files that Git considers text files.
+* text=auto eol=lf
+*.png filter=lfs diff=lfs merge=lfs -text
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0af181c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+# Godot 4+ specific ignores
+.godot/
+/android/
diff --git a/.idea/.idea.Frontiers/.idea/.gitignore b/.idea/.idea.Frontiers/.idea/.gitignore
new file mode 100644
index 0000000..df64557
--- /dev/null
+++ b/.idea/.idea.Frontiers/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/projectSettingsUpdater.xml
+/.idea.Frontiers.iml
+/modules.xml
+/contentModel.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.Frontiers/.idea/.name b/.idea/.idea.Frontiers/.idea/.name
new file mode 100644
index 0000000..ff0474e
--- /dev/null
+++ b/.idea/.idea.Frontiers/.idea/.name
@@ -0,0 +1 @@
+Frontiers
\ No newline at end of file
diff --git a/.idea/.idea.Frontiers/.idea/encodings.xml b/.idea/.idea.Frontiers/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/.idea.Frontiers/.idea/encodings.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Frontiers/.idea/indexLayout.xml b/.idea/.idea.Frontiers/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.Frontiers/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Frontiers/.idea/libraries/GdSdk_Master.xml b/.idea/.idea.Frontiers/.idea/libraries/GdSdk_Master.xml
new file mode 100644
index 0000000..6875f0b
--- /dev/null
+++ b/.idea/.idea.Frontiers/.idea/libraries/GdSdk_Master.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/.idea.Frontiers/.idea/vcs.xml b/.idea/.idea.Frontiers/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/.idea.Frontiers/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Frontiers.csproj b/Frontiers.csproj
new file mode 100644
index 0000000..8eebc1d
--- /dev/null
+++ b/Frontiers.csproj
@@ -0,0 +1,6 @@
+
+
+ net8.0
+ true
+
+
\ No newline at end of file
diff --git a/Frontiers.csproj.DotSettings b/Frontiers.csproj.DotSettings
new file mode 100644
index 0000000..2d39c02
--- /dev/null
+++ b/Frontiers.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ True
\ No newline at end of file
diff --git a/Frontiers.sln b/Frontiers.sln
new file mode 100644
index 0000000..9140ce2
--- /dev/null
+++ b/Frontiers.sln
@@ -0,0 +1,19 @@
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frontiers", "Frontiers.csproj", "{F33DFCC0-769F-49F6-9FEF-F64EF49FBEAD}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ ExportDebug|Any CPU = ExportDebug|Any CPU
+ ExportRelease|Any CPU = ExportRelease|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {F33DFCC0-769F-49F6-9FEF-F64EF49FBEAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F33DFCC0-769F-49F6-9FEF-F64EF49FBEAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F33DFCC0-769F-49F6-9FEF-F64EF49FBEAD}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
+ {F33DFCC0-769F-49F6-9FEF-F64EF49FBEAD}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
+ {F33DFCC0-769F-49F6-9FEF-F64EF49FBEAD}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
+ {F33DFCC0-769F-49F6-9FEF-F64EF49FBEAD}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/Frontiers.sln.DotSettings.user b/Frontiers.sln.DotSettings.user
new file mode 100644
index 0000000..8d1f46e
--- /dev/null
+++ b/Frontiers.sln.DotSettings.user
@@ -0,0 +1,2 @@
+
+ ForceIncluded
\ No newline at end of file
diff --git a/Source/CameraController.cs b/Source/CameraController.cs
new file mode 100644
index 0000000..e1efe4e
--- /dev/null
+++ b/Source/CameraController.cs
@@ -0,0 +1,117 @@
+using Godot;
+
+public partial class CameraController : Camera2D
+{
+ [Export] public float ZoomSpeed = 1.1f;
+ [Export] public float MinZoom = 0.01f;
+ [Export] public float MaxZoom = 5.0f;
+ [Export] public bool EnableRotation = false;
+ [Export] public float RotationSpeed = 0.01f;
+ [Export] public float PanSmoothing = 0.1f;
+
+ private bool _isDragging = false;
+ private Vector2 _dragStartMousePos;
+ private Vector2 _dragStartCameraPos;
+ private bool _isRotating = false;
+ private Vector2 _lastMousePosition;
+
+ public override void _Ready()
+ {
+ Enabled = true;
+ }
+
+ public override void _UnhandledInput(InputEvent @event)
+ {
+ HandleZoom(@event);
+ HandlePanning(@event);
+ HandleRotation(@event);
+ }
+
+ private void HandleZoom(InputEvent @event)
+ {
+ if (@event is InputEventMouseButton mouseButton && mouseButton.Pressed)
+ {
+ Vector2 mouseWorldPos = GetGlobalMousePosition();
+
+ if (mouseButton.ButtonIndex == MouseButton.WheelUp)
+ {
+ var newZoom = Zoom * ZoomSpeed;
+ if (newZoom.X <= MaxZoom)
+ {
+ ZoomToPoint(newZoom, mouseWorldPos);
+ }
+ }
+ else if (mouseButton.ButtonIndex == MouseButton.WheelDown)
+ {
+ var newZoom = Zoom / ZoomSpeed;
+ if (newZoom.X >= MinZoom)
+ {
+ ZoomToPoint(newZoom, mouseWorldPos);
+ }
+ }
+ }
+ }
+
+ private void ZoomToPoint(Vector2 newZoom, Vector2 worldPoint)
+ {
+ Vector2 viewportCenter = GetViewportRect().Size / 2;
+ Vector2 offsetFromCenter = (GetGlobalMousePosition() - GlobalPosition);
+
+ Zoom = newZoom;
+
+ Vector2 newOffsetFromCenter = offsetFromCenter / (newZoom.X / Zoom.X);
+ GlobalPosition += offsetFromCenter - newOffsetFromCenter;
+ }
+
+ private void HandlePanning(InputEvent @event)
+ {
+ if (@event is InputEventMouseButton mouseButton)
+ {
+ if (mouseButton.ButtonIndex == MouseButton.Left)
+ {
+ if (mouseButton.Pressed)
+ {
+ _isDragging = true;
+ _dragStartMousePos = mouseButton.GlobalPosition;
+ _dragStartCameraPos = GlobalPosition;
+ }
+ else
+ {
+ _isDragging = false;
+ }
+ }
+ }
+ else if (@event is InputEventMouseMotion mouseMotion && _isDragging)
+ {
+ Vector2 mouseDelta = _dragStartMousePos - mouseMotion.GlobalPosition;
+ GlobalPosition = _dragStartCameraPos + mouseDelta / Zoom;
+ }
+ }
+
+ private void HandleRotation(InputEvent @event)
+ {
+ if (!EnableRotation) return;
+
+ if (@event is InputEventMouseButton mouseButton)
+ {
+ if (mouseButton.ButtonIndex == MouseButton.Right)
+ {
+ if (mouseButton.Pressed)
+ {
+ _isRotating = true;
+ _lastMousePosition = mouseButton.GlobalPosition;
+ }
+ else
+ {
+ _isRotating = false;
+ }
+ }
+ }
+ else if (@event is InputEventMouseMotion mouseMotion && _isRotating)
+ {
+ var delta = _lastMousePosition - mouseMotion.GlobalPosition;
+ Rotation += delta.X * RotationSpeed;
+ _lastMousePosition = mouseMotion.GlobalPosition;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/CameraController.cs.uid b/Source/CameraController.cs.uid
new file mode 100644
index 0000000..02a2a2b
--- /dev/null
+++ b/Source/CameraController.cs.uid
@@ -0,0 +1 @@
+uid://ceahximwi24jm
diff --git a/Source/Tile.cs b/Source/Tile.cs
new file mode 100644
index 0000000..7d71cb0
--- /dev/null
+++ b/Source/Tile.cs
@@ -0,0 +1,284 @@
+using System;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Godot;
+
+public partial class Tile : Node2D
+{
+ private Vector2I _size;
+ private Vector2I _chunkId;
+ private World _world;
+ public CancellationToken CancellationToken { get; set; }
+
+ public Tile(Vector2I size, Vector2I chunkId, World world)
+ {
+ _size = size;
+ _chunkId = chunkId;
+ _world = world;
+ }
+
+ public override void _Ready()
+ {
+ loadTile();
+ }
+
+ void loadTile()
+ {
+
+ Image image = Image.CreateEmpty(_size.X, _size.Y, false, Image.Format.Rgba8);
+
+ Vector2I heightMapResolution = new Vector2I(
+ _world.HeightMapImage.GetWidth(),
+ _world.HeightMapImage.GetHeight()
+ );
+
+ Vector2I mapResolution = _world.ChunksPerAxis * _world.TileSize;
+ Vector2I tileOffset = new Vector2I(
+ _chunkId.X * _world.TileSize,
+ _chunkId.Y * _world.TileSize
+ );
+
+ // Store parameters locally to avoid changes during generation
+ float rockThreshold = _world.RockThreshold;
+ float snowHeightThreshold = _world.SnowHeightThreshold;
+ float waterThreshold = _world.WaterThreshold;
+ float desertMoistureThreshold = _world.DesertMoistureThreshold;
+ float desertTemperatureThreshold = _world.DesertTemperatureThreshold;
+
+ DebugType debugType = _world.DebugType;
+
+ // Run in thread
+ Task.Run(() =>
+ {
+ try
+ {
+ for (int x = 0; x < _size.X; x++)
+ {
+ if (CancellationToken.IsCancellationRequested) return;
+
+ for (int y = 0; y < _size.Y; y++)
+ {
+ if (CancellationToken.IsCancellationRequested) return;
+
+ Vector2I pixelOffset = new Vector2I(
+ x + tileOffset.X,
+ y + tileOffset.Y
+ );
+
+ float heightValue = GetValueAtMapPosition(pixelOffset);
+ float heatValue = GetHeatAtMapPosition(pixelOffset);
+ float moistureValue = GetMoistureAtMapPosition(pixelOffset);
+
+ // Get the max difference between the coord and its neighbors
+ float maxDiff = 0.0f;
+ float centerHeight = heightValue;
+ bool bordersSea = false;
+ for (int offsetX = -1; offsetX <= 1; offsetX++)
+ {
+ for (int offsetY = -1; offsetY <= 1; offsetY++)
+ {
+ if (offsetX == 0 && offsetY == 0)
+ continue;
+
+ Vector2I neighborCoords = new Vector2I(
+ pixelOffset.X + offsetX,
+ pixelOffset.Y + offsetY
+ );
+
+ float neighborHeight = GetValueAtMapPosition(neighborCoords);
+
+ if (IsSeaAtMapPosition(neighborCoords, waterThreshold))
+ {
+ bordersSea = true;
+ }
+
+ float diff = Mathf.Abs(centerHeight - neighborHeight);
+ if (diff > maxDiff)
+ {
+ maxDiff = diff;
+ }
+ }
+ }
+
+
+
+ if (debugType == DebugType.None)
+ {
+
+ // Grass (default)
+ image.SetPixel(x, y, Color.FromString("#bed58a", Colors.Purple));
+
+ if (IsSeaAtMapPosition(pixelOffset, waterThreshold))
+ {
+ // Water
+ image.SetPixel(x, y, Color.FromString("#4380b0", Colors.Purple));
+ continue;
+ }
+
+ if (moistureValue < desertMoistureThreshold && heatValue > desertTemperatureThreshold)
+ {
+ // Desert
+ image.SetPixel(x, y, Color.FromString("#edc9af", Colors.Purple));
+ }
+
+ if (bordersSea)
+ {
+ // Beach
+ image.SetPixel(x, y, Color.FromString("#808080", Colors.Purple));
+ continue;
+ }
+
+ if (heightValue > snowHeightThreshold)
+ {
+ // Snow
+ image.SetPixel(x, y, Color.FromString("#f4f4f4", Colors.Purple));
+ }
+
+ // We want steep areas to be grey for rock.
+ if (maxDiff > rockThreshold)
+ {
+ // Steep area
+ image.SetPixel(x, y, Color.FromString("#e4d3a6", Colors.Purple));
+ continue;
+ }
+
+ }
+ else if (debugType == DebugType.Height)
+ {
+ float grayValue = heightValue;
+ image.SetPixel(x, y, new Color(grayValue, grayValue, grayValue));
+ }
+ else if (debugType == DebugType.Slope)
+ {
+ float grayValue = maxDiff * 5.0f;
+ image.SetPixel(x, y, new Color(grayValue, grayValue, grayValue));
+ } else if (debugType == DebugType.Temperature)
+ {
+ image.SetPixel(x, y, new Color(heatValue, heatValue, heatValue));
+ } else if (debugType == DebugType.Moisture)
+ {
+ image.SetPixel(x, y, new Color(moistureValue, moistureValue, moistureValue));
+ }
+
+
+
+
+ }
+ }
+
+ if (!CancellationToken.IsCancellationRequested)
+ {
+ // Create a texture from the image and assign it to a Sprite2D
+ Texture2D texture = ImageTexture.CreateFromImage(image);
+ Sprite2D sprite = new Sprite2D();
+ sprite.Texture = texture;
+ sprite.TextureFilter = TextureFilterEnum.Nearest;
+ CallDeferred("add_child", sprite);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // Generation was cancelled, do nothing
+ }
+ }, CancellationToken);
+
+ }
+
+ // Terrain Query Methods
+ private float GetValueAtMapPosition(Vector2I position)
+ {
+
+ // Clamp position to map bounds
+ position.X = Mathf.Clamp(position.X, 0, _world.ChunksPerAxis.X * _world.TileSize - 1);
+ position.Y = Mathf.Clamp(position.Y, 0, _world.ChunksPerAxis.Y * _world.TileSize - 1);
+
+ Vector2I heightMapResolution = new Vector2I(
+ _world.HeightMapImage.GetWidth(),
+ _world.HeightMapImage.GetHeight()
+ );
+
+ Vector2I mapResolution = _world.ChunksPerAxis * _world.TileSize;
+
+ Vector2 mapDelta = (Vector2) position / (Vector2) mapResolution;
+
+ Vector2I heightMapCoords = new Vector2I(
+ (int)(mapDelta.X * heightMapResolution.X),
+ (int)(mapDelta.Y * heightMapResolution.Y)
+ );
+
+ Color hmColor = _world.HeightMapImage.GetPixel(heightMapCoords.X, heightMapCoords.Y);
+
+ return hmColor.R;
+ }
+
+ private bool isSeaAtMapPosition(Vector2I position)
+ {
+ float heightValue = GetValueAtMapPosition(position);
+ return heightValue <= _world.WaterThreshold;
+ }
+
+ private bool IsSeaAtMapPosition(Vector2I position, float waterThreshold)
+ {
+ float heightValue = GetValueAtMapPosition(position);
+ return heightValue <= waterThreshold;
+ }
+
+ private FastNoiseLite _heatNoise = null;
+ private float GetHeatAtMapPosition(Vector2I position)
+ {
+
+ if (_heatNoise == null)
+ {
+ _heatNoise = new FastNoiseLite();
+ _heatNoise.SetNoiseType(FastNoiseLite.NoiseTypeEnum.Perlin);
+ _heatNoise.SetFrequency(1/_world.TemperatureNoiseFrequency);
+ _heatNoise.SetSeed(1738);
+ }
+
+ float heat = _heatNoise.GetNoise2D(position.X, position.Y);
+ heat *= 0.2f;
+
+ // Fall off towards the poles. Use sine function for a globe effect.
+ float mapResolutionY = _world.ChunksPerAxis.Y * _world.TileSize;
+ float latitudeFactor = Mathf.Sin(Mathf.Pi * position.Y / mapResolutionY);
+ heat = latitudeFactor + heat;
+ heat = Mathf.Clamp(
+ heat,
+ 0.0f,
+ 1.0f
+ );
+
+ return heat;
+ }
+
+ private FastNoiseLite _moistureNoise = null;
+ private float GetMoistureAtMapPosition(Vector2I position)
+ {
+
+ if (_moistureNoise == null)
+ {
+ _moistureNoise = new FastNoiseLite();
+ _moistureNoise.SetNoiseType(FastNoiseLite.NoiseTypeEnum.Perlin);
+ _moistureNoise.SetFrequency(1/_world.MoistureNoiseFrequency);
+ _moistureNoise.SetSeed(1337);
+ }
+
+ float moisture = _moistureNoise.GetNoise2D(position.X, position.Y);
+ moisture *= 0.2f;
+
+ // Fall off towards the poles. Use sine function for a globe effect.
+ float mapResolutionY = _world.ChunksPerAxis.Y * _world.TileSize;
+ float latitudeFactor = Mathf.Sin(Mathf.Pi * position.Y / mapResolutionY);
+ moisture = latitudeFactor + moisture;
+ moisture = Mathf.Clamp(
+ moisture,
+ 0.0f,
+ 1.0f
+ );
+
+
+ return moisture;
+ }
+
+}
diff --git a/Source/Tile.cs.uid b/Source/Tile.cs.uid
new file mode 100644
index 0000000..d0412b8
--- /dev/null
+++ b/Source/Tile.cs.uid
@@ -0,0 +1 @@
+uid://d1af74387s3fk
diff --git a/Source/World.cs b/Source/World.cs
new file mode 100644
index 0000000..9f7b88a
--- /dev/null
+++ b/Source/World.cs
@@ -0,0 +1,289 @@
+using Godot;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Threading;
+using System.Threading.Tasks;
+using Godot.Collections;
+
+public enum DebugType
+{
+ None,
+ Slope,
+ Height,
+ Temperature,
+ Moisture
+}
+
+[Tool]
+public partial class World : Node2D
+{
+
+
+ private int _chunksX = 32;
+ [Export]
+ public int ChunkXCount
+ {
+ get => _chunksX;
+ set
+ {
+ _chunksX = value;
+ LoadTiles();
+ }
+ }
+
+ public Vector2I ChunksPerAxis
+ {
+ get
+ {
+
+ if (HeightMapImage == null)
+ {
+ return new Vector2I(_chunksX, _chunksX);
+ }
+
+ Vector2I heightMapResolution = new Vector2I(HeightMapImage.GetWidth(), HeightMapImage.GetHeight());
+ int chunksY = (int)(heightMapResolution.Y / (heightMapResolution.X / _chunksX));
+ return new Vector2I(_chunksX, chunksY);
+ }
+ }
+
+ private int _tileSize = 96;
+ [Export]
+ public int TileSize
+ {
+ get => _tileSize;
+ set
+ {
+ _tileSize = value;
+ LoadTiles();
+ }
+ }
+
+ [Export]
+ public Texture2D HeightMapTexture;
+
+ private float _rockThreshold = 0.05f;
+ [Export]
+ public float RockThreshold
+ {
+ get => _rockThreshold;
+ set
+ {
+ _rockThreshold = value;
+ LoadTiles();
+ }
+ }
+
+ private float _snowHeightThreshold = 0.8f;
+ [Export]
+ public float SnowHeightThreshold
+ {
+ get => _snowHeightThreshold;
+ set
+ {
+ _snowHeightThreshold = value;
+ LoadTiles();
+ }
+ }
+
+ private float _waterThreshold = 0.0f;
+ [Export]
+ public float WaterThreshold
+ {
+ get => _waterThreshold;
+ set
+ {
+ _waterThreshold = value;
+ LoadTiles();
+ }
+ }
+
+ private float _desertTemperatureThreshold = 0.7f;
+ [Export]
+ public float DesertTemperatureThreshold
+ {
+ get => _desertTemperatureThreshold;
+ set
+ {
+ _desertTemperatureThreshold = value;
+ LoadTiles();
+ }
+ }
+
+ private float _desertMoistureThreshold = 0.3f;
+ [Export]
+ public float DesertMoistureThreshold
+ {
+ get => _desertMoistureThreshold;
+ set
+ {
+ _desertMoistureThreshold = value;
+ LoadTiles();
+ }
+ }
+
+ private float _temperatureNoiseFrequency = 0.01f;
+ [Export]
+ public float TemperatureNoiseFrequency
+ {
+ get => _temperatureNoiseFrequency;
+ set
+ {
+ _temperatureNoiseFrequency = value;
+ LoadTiles();
+ }
+ }
+
+ private float _moistureNoiseFrequency = 0.01f;
+ [Export]
+ public float MoistureNoiseFrequency
+ {
+ get => _moistureNoiseFrequency;
+ set
+ {
+ _moistureNoiseFrequency = value;
+ LoadTiles();
+ }
+ }
+
+ private DebugType _debugType = DebugType.None;
+ [Export]
+ public DebugType DebugType
+ {
+ get => _debugType;
+ set
+ {
+ _debugType = value;
+ LoadTiles();
+ }
+ }
+
+
+
+ ConcurrentDictionary _tiles = new ConcurrentDictionary();
+ private CancellationTokenSource _cancellationTokenSource;
+
+ public World()
+ {
+
+ }
+
+ public override void _Ready()
+ {
+
+ // If the height map image is not assigned, log an error and return
+ if (HeightMapTexture == null)
+ {
+ GD.PrintErr("Height map image is not assigned.");
+ return;
+ }
+
+ LoadTiles();
+ }
+
+ private void LoadTiles()
+ {
+ // Check if HeightMapImage is available
+ if (HeightMapImage == null)
+ {
+ GD.PrintErr("Cannot load tiles: HeightMapImage is null");
+ return;
+ }
+
+ // Cancel any existing generation
+ _cancellationTokenSource?.Cancel();
+ _cancellationTokenSource = new CancellationTokenSource();
+ var cancellationToken = _cancellationTokenSource.Token;
+
+ // Clear existing tiles
+ foreach (var tile in _tiles.Values)
+ {
+ tile.Visible = false;
+
+ // Delete and delete its children
+ foreach (Node child in tile.GetChildren())
+ {
+ child.QueueFree();
+ }
+ tile.QueueFree();
+
+ }
+ _tiles.Clear();
+
+ int scale = 1;
+
+ // Store local copies of parameters to avoid them changing mid-generation
+ int tileSize = TileSize;
+ int chunksX = _chunksX;
+
+ // Get the resolution of the height map image
+ Vector2I heightMapResolution = new Vector2I(HeightMapImage.GetWidth(), HeightMapImage.GetHeight());
+
+ // Calculate the number of chunks in the Y direction based on the aspect ratio
+ int chunksY = (int)(heightMapResolution.Y / (heightMapResolution.X / _chunksX));
+
+ // Loop through each chunk position and create a Tile
+ Task.Run(() =>
+ {
+ try
+ {
+ for (int x = 0; x < chunksX; x++)
+ {
+ if (cancellationToken.IsCancellationRequested) return;
+
+ Parallel.For(0, chunksY, new ParallelOptions { CancellationToken = cancellationToken }, y =>
+ {
+ if (cancellationToken.IsCancellationRequested) return;
+
+ Vector2I chunkId = new Vector2I(x, y);
+
+ Tile tile = new Tile(new Vector2I(tileSize, tileSize), chunkId, this);
+ tile.CancellationToken = cancellationToken;
+ tile.Position = new Vector2(chunkId.X * tileSize, chunkId.Y * tileSize) * scale;
+ tile.Scale = new Vector2(scale, scale);
+
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ CallDeferred("add_child", tile);
+ _tiles[chunkId] = tile;
+ GD.Print("Loaded tile at chunk " + chunkId);
+ }
+ });
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ GD.Print("Tile generation cancelled");
+ }
+ }, cancellationToken);
+
+ }
+
+ private Image _heightMapCache;
+ public Image HeightMapImage
+ {
+ get
+ {
+ // If not cached, load and cache it
+ if (_heightMapCache == null && HeightMapTexture != null)
+ {
+ _heightMapCache = HeightMapTexture.GetImage();
+
+ // If GetImage() returns null, try loading directly from file
+ if (_heightMapCache == null && HeightMapTexture.ResourcePath != "")
+ {
+ _heightMapCache = Image.LoadFromFile(HeightMapTexture.ResourcePath);
+
+ if (_heightMapCache == null)
+ {
+ GD.PushError($"Could not load heightmap from {HeightMapTexture.ResourcePath}");
+ }
+ }
+ }
+
+ return _heightMapCache;
+ }
+ }
+}
diff --git a/Source/World.cs.uid b/Source/World.cs.uid
new file mode 100644
index 0000000..ac034fc
--- /dev/null
+++ b/Source/World.cs.uid
@@ -0,0 +1 @@
+uid://btqabtn0awg6k
diff --git a/assets/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png b/assets/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png
new file mode 100644
index 0000000..b263a2a
--- /dev/null
+++ b/assets/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c8535a6e76f143a8c15595aa398a86753adf7193ee22b3c0a282c3b3d14f9aba
+size 33849722
diff --git a/assets/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png.import b/assets/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png.import
new file mode 100644
index 0000000..05edc0a
--- /dev/null
+++ b/assets/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dteqog3a5k8qx"
+path="res://.godot/imported/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png-f039a3c89cf0c7ad5d067a47b56cbf8e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png"
+dest_files=["res://.godot/imported/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png-f039a3c89cf0c7ad5d067a47b56cbf8e.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/assets/World_elevation_map.png b/assets/World_elevation_map.png
new file mode 100644
index 0000000..d8e0e7a
--- /dev/null
+++ b/assets/World_elevation_map.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f91a66e40335d9d4315abee2c0ed0b155481f71c340fadca2486f3f761a06a23
+size 75282262
diff --git a/assets/World_elevation_map.png.import b/assets/World_elevation_map.png.import
new file mode 100644
index 0000000..09da52a
--- /dev/null
+++ b/assets/World_elevation_map.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cthrxmyms6rxj"
+path="res://.godot/imported/World_elevation_map.png-ffb15608784624fa8e00c287e76ba4a3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/World_elevation_map.png"
+dest_files=["res://.godot/imported/World_elevation_map.png-ffb15608784624fa8e00c287e76ba4a3.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/assets/gebco_08_rev_elev_21600x10800.png b/assets/gebco_08_rev_elev_21600x10800.png
new file mode 100644
index 0000000..2adb2c9
--- /dev/null
+++ b/assets/gebco_08_rev_elev_21600x10800.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4a70590d40dd7b9c69b9ab359d1dc4475ce97c1d2d13625223b248364112c699
+size 18414843
diff --git a/assets/gebco_08_rev_elev_21600x10800.png.import b/assets/gebco_08_rev_elev_21600x10800.png.import
new file mode 100644
index 0000000..b3662d1
--- /dev/null
+++ b/assets/gebco_08_rev_elev_21600x10800.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cfq68ehcuvoya"
+path="res://.godot/imported/gebco_08_rev_elev_21600x10800.png-f55daf6de01cca738c16dcab8f2b72f3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/gebco_08_rev_elev_21600x10800.png"
+dest_files=["res://.godot/imported/gebco_08_rev_elev_21600x10800.png-f55daf6de01cca738c16dcab8f2b72f3.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/icon.svg b/icon.svg
new file mode 100644
index 0000000..9d8b7fa
--- /dev/null
+++ b/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/icon.svg.import b/icon.svg.import
new file mode 100644
index 0000000..ddc4a7c
--- /dev/null
+++ b/icon.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://coj5oieh63qxm"
+path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://icon.svg"
+dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/main.tscn b/main.tscn
new file mode 100644
index 0000000..80adc4e
--- /dev/null
+++ b/main.tscn
@@ -0,0 +1,21 @@
+[gd_scene load_steps=4 format=3 uid="uid://c10nqwr7qp0ai"]
+
+[ext_resource type="Script" uid="uid://btqabtn0awg6k" path="res://Source/World.cs" id="1_0xm2m"]
+[ext_resource type="Script" uid="uid://ceahximwi24jm" path="res://Source/CameraController.cs" id="3_camera"]
+[ext_resource type="Texture2D" uid="uid://dteqog3a5k8qx" path="res://assets/World_Elevation_Map_8_bit_(World_Height_map)_(alterative_version).png" id="3_h2yge"]
+
+[node name="Main" type="Node2D"]
+
+[node name="CameraController" type="Camera2D" parent="."]
+script = ExtResource("3_camera")
+
+[node name="World" type="Node2D" parent="."]
+script = ExtResource("1_0xm2m")
+ChunkXCount = 16
+TileSize = 128
+HeightMapTexture = ExtResource("3_h2yge")
+RockThreshold = 0.04
+SnowHeightThreshold = 0.32
+WaterThreshold = 0.24
+DesertTemperatureThreshold = 0.82
+DesertMoistureThreshold = 0.985
diff --git a/project.godot b/project.godot
new file mode 100644
index 0000000..13840c6
--- /dev/null
+++ b/project.godot
@@ -0,0 +1,20 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Frontiers"
+run/main_scene="uid://c10nqwr7qp0ai"
+config/features=PackedStringArray("4.4", "C#", "Forward Plus")
+config/icon="res://icon.svg"
+
+[dotnet]
+
+project/assembly_name="Frontiers"