diff --git a/components/NavigationComponent.cs b/components/NavigationComponent.cs
index ae6166a..f7024bb 100644
--- a/components/NavigationComponent.cs
+++ b/components/NavigationComponent.cs
@@ -11,7 +11,7 @@ using GoDotLog;
///
///
-public class NavigationComponent : Node
+public class NavigationComponent : Spatial
{
public class NavigationPoint
{
@@ -83,6 +83,9 @@ public class NavigationComponent : Node
private Vector3 _currentGoalPositionWorld = Vector3.Zero;
private Quat _currentGoalOrientationWorld = Quat.Identity;
+ private Area _pathCollisionQueryVolume;
+ private CollisionShape _pathCollisionQueryShape;
+
private List _pathWorldNavigationPoints;
private HexCell[] _path;
@@ -90,6 +93,12 @@ public class NavigationComponent : Node
{
base._Ready();
_pathWorldNavigationPoints = new List();
+
+ _pathCollisionQueryVolume = (Area)FindNode("PathCollisionQueryVolume");
+ Debug.Assert(_pathCollisionQueryVolume != null);
+ _pathCollisionQueryShape = (CollisionShape)_pathCollisionQueryVolume.FindNode("CollisionShape");
+ Debug.Assert(_pathCollisionQueryShape != null);
+
}
public override void _Process(float delta)
@@ -158,6 +167,8 @@ public class NavigationComponent : Node
{
throw new NotImplementedException();
}
+
+ CheckPathCollision(fromTransformWorld.origin, navigationPoint.WorldPosition);
}
@@ -178,15 +189,31 @@ public class NavigationComponent : Node
}
- bool SweptSphereHasCollision(Vector3 fromPosition, Vector3 toPosition, float radius)
+ bool CheckPathCollision(Vector3 fromPositionWorld, Vector3 toPositionWorld)
{
- if ((fromPosition - toPosition).LengthSquared() < 0.001)
+ Vector3 fromPositionLocal = GlobalTransform.XformInv(fromPositionWorld);
+ Vector3 toPositionLocal = GlobalTransform.XformInv(toPositionWorld);
+ float distance = (toPositionLocal - fromPositionLocal).Length();
+ Vector3 direction = (toPositionLocal - fromPositionLocal) / distance;
+ Vector3 side = Vector3.Up.Cross(direction);
+ Basis orientation = new Basis(side, Vector3.Up, direction);
+
+ _pathCollisionQueryVolume.Transform =
+ new Transform(orientation, 0.5f * toPositionLocal + 0.5f * fromPositionLocal).Scaled(new Vector3(0.5f, 0.5f, distance));
+
+ var collisionBodies = _pathCollisionQueryVolume.GetOverlappingBodies();
+
+ if (collisionBodies.Count > 0)
+ {
+ GD.Print("There is a body: " + collisionBodies[0]);
+ return true;
+ }
+
+ if ((fromPositionWorld - toPositionWorld).LengthSquared() < 0.001)
{
return false;
}
- Vector3 direction = (toPosition - fromPosition).Normalized();
-
// TODO: Complete Implementation
Debug.Assert(false);
return true;
@@ -206,7 +233,7 @@ public class NavigationComponent : Node
Vector3 startPoint = navigationPoints[startIndex].WorldPosition;
Vector3 endPoint = navigationPoints[endIndex].WorldPosition;
- if (SweptSphereHasCollision(startPoint, endPoint, 0.25f))
+ if (CheckPathCollision(startPoint, endPoint))
{
smoothedPath.Add(navigationPoints[endIndex-1]);
smoothedPath.Add(navigationPoints[endIndex]);
diff --git a/entities/Player.cs b/entities/Player.cs
index 05c20b9..9d2c0cb 100644
--- a/entities/Player.cs
+++ b/entities/Player.cs
@@ -10,6 +10,8 @@ using GodotComponentTest.utils;
public class Player : Entity, IInteractionInterface
{
// public members
+ [Export] public NodePath TileWorldNode;
+
public Vector3 TargetPosition = Vector3.Zero;
public int goldCount = 0;
@@ -47,7 +49,7 @@ public class Player : Entity, IInteractionInterface
_groundMotion = new GroundMotionComponent();
_worldInfo = (WorldInfoComponent)FindNode("WorldInfo", false);
_navigationComponent = (NavigationComponent)FindNode("Navigation", false);
- _navigationComponent.TileWorld = _worldInfo.TileWorld;
+ _navigationComponent.TileWorld = GetNode(TileWorldNode);
TaskQueueComponent = new TaskQueueComponent();
_itemAttractorArea = (Area)FindNode("ItemAttractorArea", false);
diff --git a/scenes/Game.tscn b/scenes/Game.tscn
index 5491aa2..0fdf809 100644
--- a/scenes/Game.tscn
+++ b/scenes/Game.tscn
@@ -1,12 +1,12 @@
-[gd_scene load_steps=15 format=2]
+[gd_scene load_steps=12 format=2]
-[ext_resource path="res://scenes/StreamContainer.cs" type="Script" id=1]
+[ext_resource path="res://scenes/StreamContainer.tscn" type="PackedScene" id=1]
[ext_resource path="res://entities/Player.tscn" type="PackedScene" id=2]
+[ext_resource path="res://scenes/Camera.tscn" type="PackedScene" id=3]
[ext_resource path="res://utils/TileHighlight.tscn" type="PackedScene" id=5]
[ext_resource path="res://entities/Chest.tscn" type="PackedScene" id=7]
[ext_resource path="res://scenes/TileWorld.tscn" type="PackedScene" id=8]
[ext_resource path="res://scenes/Game.cs" type="Script" id=9]
-[ext_resource path="res://scenes/DebugCamera.gd" type="Script" id=10]
[ext_resource path="res://ui/WorldGeneratorUI.gd" type="Script" id=12]
[ext_resource path="res://entities/Axe.tscn" type="PackedScene" id=14]
[ext_resource path="res://systems/InteractionSystem.cs" type="Script" id=15]
@@ -27,16 +27,6 @@ tracks/0/keys = {
"values": [ Vector2( 1, 1 ), Vector2( 2, 2 ), Vector2( 1, 1 ) ]
}
-[sub_resource type="CubeMesh" id=1]
-size = Vector3( 1, 1, 1 )
-
-[sub_resource type="SpatialMaterial" id=2]
-params_blend_mode = 3
-albedo_color = Color( 1, 1, 1, 0.156863 )
-
-[sub_resource type="BoxShape" id=9]
-extents = Vector3( 20, 1, 20 )
-
[node name="Game" type="Spatial"]
script = ExtResource( 9 )
@@ -48,6 +38,7 @@ visible = false
transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.1, 0 )
[node name="TileWorld" parent="." instance=ExtResource( 8 )]
+GenerationMapType = 0
Size = 128
[node name="GameUI" type="HBoxContainer" parent="."]
@@ -282,37 +273,15 @@ rect_min_size = Vector2( 100, 100 )
stretch_mode = 1
flip_v = true
-[node name="StreamContainer" type="Spatial" parent="."]
-transform = Transform( 1, 0, 0, 0, 1, 2.98023e-08, 0, -2.98023e-08, 1, 0, 0, -4.76837e-07 )
-script = ExtResource( 1 )
-Dimensions = Vector2( 35, 30 )
-World = NodePath("../TileWorld")
+[node name="StreamContainer" parent="." instance=ExtResource( 1 )]
-[node name="ActiveTiles" type="Spatial" parent="StreamContainer"]
-
-[node name="Bounds" type="MeshInstance" parent="StreamContainer"]
-transform = Transform( 4, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0 )
-visible = false
-mesh = SubResource( 1 )
-skeleton = NodePath("../..")
-material/0 = SubResource( 2 )
-
-[node name="Area" type="Area" parent="StreamContainer"]
-
-[node name="CollisionShape" type="CollisionShape" parent="StreamContainer/Area"]
-transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0 )
-shape = SubResource( 9 )
-
-[node name="Camera" type="Camera" parent="."]
-transform = Transform( 1, 0, 0, 0, 0.60042, 0.799685, 0, -0.799685, 0.60042, -4.76837e-07, 6.37557, 4.57224 )
-current = true
-fov = 60.0
-script = ExtResource( 10 )
+[node name="Camera" parent="." instance=ExtResource( 3 )]
[node name="InteractionSystem" type="Node" parent="."]
script = ExtResource( 15 )
[node name="Player" parent="." instance=ExtResource( 2 )]
+TileWorldNode = NodePath("../TileWorld")
[node name="Entities" type="Spatial" parent="."]
diff --git a/scenes/StreamContainer.cs b/scenes/StreamContainer.cs
index 20e8f94..e64975b 100644
--- a/scenes/StreamContainer.cs
+++ b/scenes/StreamContainer.cs
@@ -226,13 +226,11 @@ public class StreamContainer : Spatial
public void OnTileClicked(HexTile3D tile)
{
-// GD.Print("Clicked on Tile at " + tile.OffsetCoords);
EmitSignal("TileClicked", tile);
}
public void OnTileHovered(HexTile3D tile)
{
-// GD.Print("Hovered on Tile at " + tile.OffsetCoords);
EmitSignal("TileHovered", tile);
}
diff --git a/scenes/StreamContainer.gd b/scenes/StreamContainer.gd
deleted file mode 100644
index c895ed2..0000000
--- a/scenes/StreamContainer.gd
+++ /dev/null
@@ -1,91 +0,0 @@
-extends Spatial
-
-onready var hexgrid = preload("res://addons/gdhexgrid/HexGrid.gd").new()
-onready var hextile3d = preload("res://scenes/HexTile3D.tscn")
-onready var active_tiles = $ActiveTiles
-
-
-export var world_rect: Rect2 = Rect2() setget set_rect
-var offset_coord_rect: Rect2 = Rect2()
-
-var bottom_left_cell: HexCell = HexCell.new()
-var top_right_cell: HexCell = HexCell.new()
-
-var tiles = {}
-var tiles_by_offset_coord = {}
-
-onready var bounds = $Bounds
-
-
-func _ready():
- set_rect(world_rect)
-
-
-func is_hex_coord_in_rect (coord: Vector2):
- var rect_end = offset_coord_rect.end
- return coord.x >= offset_coord_rect.position.x and coord.x < offset_coord_rect.end.x and coord.y >= offset_coord_rect.position.y and coord.y < offset_coord_rect.end.y
-
-
-func instantiate_hextile3d():
- return hextile3d.instance()
-
-
-func add_hextile_to_tree(hextile3d):
- active_tiles.add_child(hextile3d)
-
-
-func create_hextile3d_at (coord: Vector2) -> HexTile3D:
- if not coord in tiles_by_offset_coord.keys():
- var new_hextile3d = instantiate_hextile3d()
- add_hextile_to_tree(new_hextile3d)
- new_hextile3d.game_tile.offset_coords = coord
- new_hextile3d.transform.origin.y = -9999
- tiles_by_offset_coord[coord] = new_hextile3d
-
- return tiles_by_offset_coord[coord]
-
-
-func cleanup_tiles():
- var num_deleted = 0
- var children = active_tiles.get_children()
- for child in children:
- var tile_offset_coords = child.game_tile.offset_coords
- if not is_hex_coord_in_rect(tile_offset_coords):
- tiles_by_offset_coord.erase(tile_offset_coords)
- child.queue_free()
- active_tiles.remove_child(child)
- num_deleted = num_deleted + 1
-
-# print ("deleted ", num_deleted, " tiles")
-
-
-func set_rect(rect: Rect2):
- world_rect = rect
-
- if bounds == null:
- return
-
- var world_rect_center = rect.get_center()
-
- bounds.transform = Transform(
- Vector3 (world_rect.size.x / 2, 0, 0),
- Vector3(0, 1, 0), Vector3(0, 0, world_rect.size.y / 2),
- Vector3(world_rect_center.x, 0, world_rect_center.y)
- )
-
- bottom_left_cell = hexgrid.get_hex_at(Vector2(rect.position.x, rect.end.y))
- top_right_cell = hexgrid.get_hex_at(Vector2(rect.end.x, rect.position.y))
-
- offset_coord_rect = Rect2(
- bottom_left_cell.offset_coords.x, bottom_left_cell.offset_coords.y,
- top_right_cell.offset_coords.x - bottom_left_cell.offset_coords.x, top_right_cell.offset_coords.y - bottom_left_cell.offset_coords.y
- )
-
-# print ("----")
-# print ("world_rect: ", world_rect)
-# print ("cells: ", bottom_left_cell.offset_coords, " to ", top_right_cell.offset_coords)
-# print ("offset_coord_rect: ", offset_coord_rect.position, " size: ", offset_coord_rect.size)
-
- cleanup_tiles()
-
- return world_rect
diff --git a/scenes/TileWorld.cs b/scenes/TileWorld.cs
index d0044ba..f9460e5 100644
--- a/scenes/TileWorld.cs
+++ b/scenes/TileWorld.cs
@@ -3,6 +3,7 @@ using System;
using System.Linq;
using System.Diagnostics;
using Godot.Collections;
+using Namespace;
using Array = Godot.Collections.Array;
using Vector2 = Godot.Vector2;
using Vector3 = Godot.Vector3;
@@ -28,7 +29,18 @@ public class TileWorld : Spatial
delegate void WorldGenerated();
// public members
+ public enum MapType
+ {
+ Noise,
+ Debug,
+ Flat,
+ }
+
+ [ExportFlagsEnum(typeof(MapType))] public MapType GenerationMapType = MapType.Debug;
+
[Export] public int Size = 64;
+ [Export] public bool DebugMap = false;
+
public float HeightScale = 2.0f;
public Image Heightmap;
public Image Colormap;
@@ -100,10 +112,15 @@ public class TileWorld : Spatial
OnMapGenerationStart();
- GenerateNoiseMap();
-// GenerateDebugMap();
-
- OnHeightMapChanged();
+ switch (GenerationMapType)
+ {
+ case MapType.Debug: GenerateDebugMap();
+ break;
+ case MapType.Flat: GenerateFlatMap();
+ break;
+ case MapType.Noise: GenerateNoiseMap();
+ break;
+ }
}
private void GenerateDebugMap()
@@ -128,7 +145,35 @@ public class TileWorld : Spatial
}
}
- Colormap.SetPixel(Size - 1, Size - 1, new Color(1, 1, 1, 1));
+ // Colormap.SetPixel(Size - 1, Size - 1, new Color(1, 1, 1, 1));
+ Colormap.Fill(Colors.ForestGreen);
+ Colormap.Unlock();
+
+ OnMapGenerationComplete();
+ }
+
+
+ private void GenerateFlatMap()
+ {
+ Colormap = new Image();
+ Colormap.Create(Size, Size, false, Image.Format.Rgba8);
+
+ Heightmap = new Image();
+ Heightmap.Create(Size, Size, false, Image.Format.Rf);
+
+ Heightmap.Lock();
+ Colormap.Lock();
+
+ foreach (int coord_x in Enumerable.Range(0, Size))
+ {
+ foreach (int coord_y in Enumerable.Range(0, Size))
+ {
+ Heightmap.SetPixel(coord_x, coord_y,
+ new Color(0, 0, 0, 1));
+ }
+ }
+
+ Colormap.Fill(Colors.ForestGreen);
Colormap.Unlock();
OnMapGenerationComplete();
@@ -163,7 +208,7 @@ public class TileWorld : Spatial
{
child.QueueFree();
}
-
+
foreach (Node child in Entities.GetChildren())
{
child.QueueFree();
@@ -329,6 +374,17 @@ public class TileWorld : Spatial
}
+ public void SetTileColorAtOffset(Vector2 offsetCoord, Color color)
+ {
+ Vector2 textureCoord = OffsetToTextureCoord(offsetCoord);
+
+ Colormap.Lock();
+ Colormap.SetPixel((int) textureCoord.x, (int) textureCoord.y, color);
+ Colormap.Unlock();
+
+ EmitSignal("WorldGenerated");
+ }
+
public Vector2 WorldToOffsetCoords(Vector3 worldCoord)
{
return _hexGrid.GetHexAt(new Vector2(worldCoord.x, worldCoord.z)).OffsetCoords;
@@ -337,13 +393,9 @@ public class TileWorld : Spatial
public Vector3 GetTileWorldCenterFromOffset(Vector2 offsetCoord)
{
- Vector2 tileCenter = _hexGrid.GetHexCenterFromOffset(offsetCoord);
+ Vector2 tileCenter = _hexGrid.GetHexCenterFromOffset(offsetCoord - Vector2.One * Mathf.Round(Size / 2f));
- // TODO: coordinates do not match for bigger maps
- return new Vector3(
- tileCenter.x - Mathf.Round(Size * 0.75f * 0.5f),
- GetHeightAtOffset(offsetCoord),
- tileCenter.y + ((Mathf.Sqrt(3) / 2) * Mathf.Round(Size * 0.5f)));
+ return new Vector3(tileCenter.x, GetHeightAtOffset(offsetCoord), tileCenter.y);
}
diff --git a/scenes/tests/EditorUI.cs b/scenes/tests/EditorUI.cs
new file mode 100644
index 0000000..cee3067
--- /dev/null
+++ b/scenes/tests/EditorUI.cs
@@ -0,0 +1,87 @@
+using Godot;
+using System;
+
+public class EditorUI : Control
+{
+ // exported members
+ [Export] public NodePath World;
+ [Export] public NodePath StreamContainer;
+
+ // public members
+ public Vector2 currentTileOffset = Vector2.Zero;
+
+ // private members
+ private Button _resetButton;
+ private Button _grassButton;
+ private Button _sandButton;
+ private Button _waterButton;
+
+ private TileWorld _tileWorld;
+ private StreamContainer _streamContainer;
+
+ private enum TileType
+ {
+ None,
+ Grass,
+ Sand,
+ Water
+ }
+ private TileType _currentTileType = TileType.None;
+
+ // Called when the node enters the scene tree for the first time.
+ public override void _Ready()
+ {
+ _tileWorld = (TileWorld) GetNode(World);
+ _streamContainer = (StreamContainer)GetNode(StreamContainer);
+
+ // signals
+ _resetButton = (Button) FindNode("ResetButton");
+ _resetButton.Connect("pressed", this, nameof(OnResetButton));
+
+ _grassButton = (Button) FindNode("GrassButton");
+ _grassButton.Connect("pressed", this, nameof(OnGrassButton));
+
+ _sandButton = (Button) FindNode("SandButton");
+ _sandButton.Connect("pressed", this, nameof(OnSandButton));
+
+ _waterButton = (Button) FindNode("WaterButton");
+ _waterButton.Connect("pressed", this, nameof(OnWaterButton));
+ }
+
+
+ public void OnResetButton()
+ {
+ GD.Print("Resetting Map");
+ _tileWorld.Seed = _tileWorld.Seed + 1;
+ _tileWorld.Generate(12);
+ }
+
+ public void OnGrassButton()
+ {
+ _currentTileType = TileType.Grass;
+ }
+
+ public void OnSandButton()
+ {
+ _currentTileType = TileType.Sand;
+
+ }
+
+ public void OnWaterButton()
+ {
+ _currentTileType = TileType.Water;
+ }
+
+ public void OnTileClicked(Vector2 offsetCoord)
+ {
+ switch (_currentTileType)
+ {
+ case TileType.Grass:_tileWorld.SetTileColorAtOffset(currentTileOffset, Colors.Green);
+ break;
+ case TileType.Water:_tileWorld.SetTileColorAtOffset(currentTileOffset, Colors.Blue);
+ break;
+ case TileType.Sand:_tileWorld.SetTileColorAtOffset(currentTileOffset, Colors.Yellow);
+ break;
+ }
+ }
+}
diff --git a/scenes/tests/NavigationTests.cs b/scenes/tests/NavigationTests.cs
new file mode 100644
index 0000000..0990155
--- /dev/null
+++ b/scenes/tests/NavigationTests.cs
@@ -0,0 +1,115 @@
+using Godot;
+using System;
+
+public class NavigationTests : Spatial
+{
+ // Declare member variables here. Examples:
+ // private int a = 2;
+ // private string b = "text";
+
+ private StaticBody _groundLayer;
+ private HexGrid _hexGrid;
+ private HexCell _currentTile;
+ private HexCell _lastTile;
+
+ private Spatial _mouseHighlight;
+ private ShaderMaterial _tileMaterial;
+
+ private EditorUI _editorUi;
+ private TileWorld _tileWorld;
+ private StreamContainer _streamContainer;
+ private Player _player;
+
+ // Called when the node enters the scene tree for the first time.
+ public override void _Ready()
+ {
+ _hexGrid = new HexGrid();
+ _currentTile = new HexCell();
+ _lastTile = new HexCell();
+
+ _tileMaterial = GD.Load("materials/HexTileTextureLookup.tres");
+
+ _groundLayer = GetNode("GroundLayer");
+
+ _mouseHighlight = GetNode("MouseHighlight");
+
+ _editorUi = GetNode("EditorUI");
+ _tileWorld = GetNode("TileWorld");
+ _tileWorld.Connect("WorldGenerated", this, nameof(OnWorldGenerated));
+ _streamContainer = GetNode("StreamContainer");
+
+ _streamContainer.SetCenterTile(_currentTile);
+
+ _player = GetNode("Player");
+
+ // input handling
+ _groundLayer.Connect("input_event", this, nameof(OnGroundLayerInputEvent));
+ _streamContainer.Connect("TileClicked", this, nameof(OnTileClicked));
+ _streamContainer.Connect("TileHovered", this, nameof(OnTileHovered));
+ }
+
+ public void OnWorldGenerated()
+ {
+ _streamContainer.OnWorldGenerated();
+
+ // Properly place the Player
+ Vector2 centerTileCoord = (Vector2.One * _tileWorld.Size / 2).Round();
+ Vector3 worldCenterTileCoords = _tileWorld.GetTileWorldCenterFromOffset(centerTileCoord);
+ worldCenterTileCoords.y = _tileWorld.GetHeightAtOffset(centerTileCoord);
+ Transform playerTransform = Transform.Identity;
+ playerTransform.origin = worldCenterTileCoords;
+ _player.Transform = playerTransform;
+
+ ImageTexture newWorldTexture = new ImageTexture();
+ newWorldTexture.CreateFromImage(_tileWorld.Colormap,
+ (uint)(Texture.FlagsEnum.Mipmaps | Texture.FlagsEnum.Repeat));
+ _tileMaterial.SetShaderParam("MapAlbedoTexture", newWorldTexture);
+ _tileMaterial.SetShaderParam("TextureSize", (int)_tileWorld.Colormap.GetSize().x);
+ }
+
+
+ public void UpdateCurrentTile(HexCell tile)
+ {
+ if (_currentTile.AxialCoords == tile.AxialCoords)
+ {
+ return;
+ }
+
+ _lastTile = _currentTile;
+ _currentTile = tile;
+
+ GD.Print("Current tile: " + _currentTile.OffsetCoords);
+
+ if (_lastTile.OffsetCoords != _currentTile.OffsetCoords && _editorUi != null)
+ {
+ _editorUi.currentTileOffset = _currentTile.OffsetCoords;
+ }
+
+ Vector2 planeCoords = _hexGrid.GetHexCenterFromOffset(_currentTile.OffsetCoords);
+ Transform tileTransform = Transform.Identity;
+ tileTransform.origin.x = planeCoords.x;
+ tileTransform.origin.y = _tileWorld.GetHeightAtOffset(_currentTile.OffsetCoords) + 0.1f;
+ tileTransform.origin.z = planeCoords.y;
+
+ _mouseHighlight.Transform = tileTransform;
+ }
+
+ public void OnGroundLayerInputEvent(Node camera, InputEvent inputEvent, Vector3 position, Vector3 normal,
+ int shapeIndex)
+ {
+ UpdateCurrentTile(_hexGrid.GetHexAt(new Vector2(position.x, position.z)));
+ }
+
+ public void OnTileClicked(HexTile3D tile)
+ {
+ if (_editorUi != null)
+ {
+ _editorUi.OnTileClicked(tile.OffsetCoords);
+ }
+ }
+
+ public void OnTileHovered(HexTile3D tile)
+ {
+ UpdateCurrentTile(tile.Cell);
+ }
+}
diff --git a/scenes/tests/NavigationTests.tscn b/scenes/tests/NavigationTests.tscn
new file mode 100644
index 0000000..50046a5
--- /dev/null
+++ b/scenes/tests/NavigationTests.tscn
@@ -0,0 +1,101 @@
+[gd_scene load_steps=12 format=2]
+
+[ext_resource path="res://entities/Player.tscn" type="PackedScene" id=1]
+[ext_resource path="res://scenes/TileWorld.tscn" type="PackedScene" id=2]
+[ext_resource path="res://utils/TileHighlight.tscn" type="PackedScene" id=3]
+[ext_resource path="res://scenes/tests/NavigationTests.cs" type="Script" id=4]
+[ext_resource path="res://scenes/StreamContainer.tscn" type="PackedScene" id=5]
+[ext_resource path="res://scenes/Camera.tscn" type="PackedScene" id=6]
+[ext_resource path="res://scenes/tests/EditorUI.cs" type="Script" id=7]
+
+[sub_resource type="ButtonGroup" id=4]
+resource_local_to_scene = false
+resource_name = "TileTypeButtonGroup"
+
+[sub_resource type="BoxShape" id=1]
+extents = Vector3( 50, 1, 50 )
+
+[sub_resource type="SpatialMaterial" id=3]
+albedo_color = Color( 0.180392, 0.384314, 0.0235294, 1 )
+
+[sub_resource type="CubeMesh" id=2]
+material = SubResource( 3 )
+
+[node name="NavigationTests" type="Spatial"]
+script = ExtResource( 4 )
+
+[node name="EditorUI" type="Control" parent="."]
+margin_right = 40.0
+margin_bottom = 40.0
+script = ExtResource( 7 )
+World = NodePath("../TileWorld")
+StreamContainer = NodePath("../StreamContainer")
+
+[node name="HBoxContainer" type="HBoxContainer" parent="EditorUI"]
+margin_left = 5.0
+margin_top = 5.0
+margin_right = 40.0
+margin_bottom = 40.0
+
+[node name="ResetButton" type="Button" parent="EditorUI/HBoxContainer"]
+margin_right = 48.0
+margin_bottom = 35.0
+text = "Reset"
+
+[node name="ModeLabel" type="Label" parent="EditorUI/HBoxContainer"]
+margin_left = 52.0
+margin_top = 10.0
+margin_right = 88.0
+margin_bottom = 24.0
+text = "Mode"
+
+[node name="GrassButton" type="Button" parent="EditorUI/HBoxContainer"]
+margin_left = 92.0
+margin_right = 140.0
+margin_bottom = 35.0
+toggle_mode = true
+group = SubResource( 4 )
+text = "Grass"
+
+[node name="WaterButton" type="Button" parent="EditorUI/HBoxContainer"]
+margin_left = 144.0
+margin_right = 194.0
+margin_bottom = 35.0
+toggle_mode = true
+group = SubResource( 4 )
+text = "Water"
+
+[node name="SandButton" type="Button" parent="EditorUI/HBoxContainer"]
+margin_left = 198.0
+margin_right = 240.0
+margin_bottom = 35.0
+toggle_mode = true
+group = SubResource( 4 )
+text = "Sand"
+
+[node name="Player" parent="." instance=ExtResource( 1 )]
+TileWorldNode = NodePath("../TileWorld")
+
+[node name="GroundLayer" type="StaticBody" parent="."]
+
+[node name="CollisionShape" type="CollisionShape" parent="GroundLayer"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0 )
+shape = SubResource( 1 )
+
+[node name="MeshInstance" type="MeshInstance" parent="GroundLayer"]
+transform = Transform( 50, 0, 0, 0, 1, 0, 0, 0, 50, 0, -1.05, 0 )
+mesh = SubResource( 2 )
+
+[node name="TileWorld" parent="." instance=ExtResource( 2 )]
+GenerationMapType = 2
+Size = 20
+DebugMap = true
+
+[node name="MouseHighlight" parent="." instance=ExtResource( 3 )]
+
+[node name="StreamContainer" parent="." instance=ExtResource( 5 )]
+
+[node name="Camera" parent="." instance=ExtResource( 6 )]
+transform = Transform( 1, 0, 0, 0, 0.60042, 0.799685, 0, -0.799685, 0.60042, -4.76837e-07, 9.56665, 7.86873 )
+
+[editable path="Player"]
diff --git a/utils/ExportFlagsEnumAttribute.cs b/utils/ExportFlagsEnumAttribute.cs
new file mode 100644
index 0000000..e941efe
--- /dev/null
+++ b/utils/ExportFlagsEnumAttribute.cs
@@ -0,0 +1,47 @@
+// Source: https://gist.github.com/kleonc/a2bab51686ac6f4d7cb28aec88efa5d9
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Godot;
+
+namespace Namespace
+{
+ using UnderlyingType = UInt64;
+
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+ public class ExportFlagsEnumAttribute : ExportAttribute
+ {
+ public ExportFlagsEnumAttribute(Type enumType)
+ : base(PropertyHint.Flags, GetFlagsEnumHintString(enumType))
+ { }
+
+ private static string GetFlagsEnumHintString(Type enumType)
+ {
+ Dictionary> flagNamesByFlag = new Dictionary>();
+ UnderlyingType flag = (UnderlyingType)1;
+ foreach (string name in Enum.GetNames(enumType))
+ {
+ UnderlyingType value = (UnderlyingType)Convert.ChangeType(Enum.Parse(enumType, name), typeof(UnderlyingType));
+ while (value > flag)
+ {
+ if (!flagNamesByFlag.ContainsKey(flag))
+ {
+ flagNamesByFlag.Add(flag, new List());
+ }
+ flag <<= 1;
+ }
+ if (value == flag)
+ {
+ if (!flagNamesByFlag.TryGetValue(flag, out List names))
+ {
+ names = new List();
+ flagNamesByFlag.Add(flag, names);
+ }
+ names.Add(name);
+ }
+ }
+ return string.Join(", ", flagNamesByFlag.Values.Select(flagNames => string.Join(" / ", flagNames)));
+ }
+ }
+}
\ No newline at end of file