瀏覽代碼

Initial commit

jamesperet 3 年之前
當前提交
59f12b6e78

+ 71 - 0
.gitignore

@@ -0,0 +1,71 @@
+# This .gitignore file should be placed at the root of your Unity project directory
+#
+# Get latest from https://github.com/github/gitignore/blob/master/Unity.gitignore
+#
+/[Ll]ibrary/
+/[Tt]emp/
+/[Oo]bj/
+/[Bb]uild/
+/[Bb]uilds/
+/[Ll]ogs/
+/[Uu]ser[Ss]ettings/
+
+# MemoryCaptures can get excessive in size.
+# They also could contain extremely sensitive data
+/[Mm]emoryCaptures/
+
+# Asset meta data should only be ignored when the corresponding asset is also ignored
+!/[Aa]ssets/**/*.meta
+
+# Uncomment this line if you wish to ignore the asset store tools plugin
+# /[Aa]ssets/AssetStoreTools*
+
+# Autogenerated Jetbrains Rider plugin
+/[Aa]ssets/Plugins/Editor/JetBrains*
+
+# Visual Studio cache directory
+.vs/
+
+# Gradle cache directory
+.gradle/
+
+# Autogenerated VS/MD/Consulo solution and project files
+ExportedObj/
+.consulo/
+*.csproj
+*.unityproj
+*.sln
+*.suo
+*.tmp
+*.user
+*.userprefs
+*.pidb
+*.booproj
+*.svd
+*.pdb
+*.mdb
+*.opendb
+*.VC.db
+
+# Unity3D generated meta files
+*.pidb.meta
+*.pdb.meta
+*.mdb.meta
+
+# Unity3D generated file on crash reports
+sysinfo.txt
+
+# Builds
+*.apk
+*.aab
+*.unitypackage
+
+# Crashlytics generated file
+crashlytics-build.properties
+
+# Packed Addressables
+/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
+
+# Temporary auto-generated Android Assets
+/[Aa]ssets/[Ss]treamingAssets/aa.meta
+/[Aa]ssets/[Ss]treamingAssets/aa/*

+ 5 - 0
ChangeLog.md

@@ -0,0 +1,5 @@
+# KairoEngine.Grids Change Log
+
+### v0.1.1
+
+- Removed the ``CameraTarget`` script.

+ 7 - 0
ChangeLog.md.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c40b3660e8cee734cb6a8463d67a671c
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Documentation.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 576e4dff5c1878b4680d7ae1de1f64f8
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Editor.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: e1b4e86a2fde544458acd5953850f14d
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 43 - 0
Readme.md

@@ -0,0 +1,43 @@
+# 📦 KairoEngine.Grids v0.1.2
+
+### 🛑Required packages
+
+- `KairoEngine.Core`
+
+### 📄Namespaces
+
+- `KairoEngine.Grids`
+
+### Class Reference
+
+- Grid
+
+- GridObject
+
+- GridBuildingSystem
+
+- PlacedObjectType
+
+- PlacedObject
+
+- PlacebleObjectRule
+  
+  - Rules:
+    
+    - EmptyCell - All cells in grid have no PlacedObjects
+    
+    - HasConnector - Both parts need to have a valid connector between two cells.
+    
+    - TargetCellHasObject - The cell represented by a position offset needs to have a certain PlacedObject
+    
+    - TargetEmptyIsCell -The cell represented by a position offset needs to be empty.
+    
+    - CellInRadius - A specified PlacedObject needs to exist in a certain radius 
+    
+    - EmptyArea - All cells in a radius need to be empty
+    
+    - ?CellOcupied - The cells in the grid are ocupied by a specific PlacedObject
+
+- PlacebleObjectRulesGroup
+
+stockpile

+ 7 - 0
Readme.md.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: c441d748eb993fd41a350a99802aa804
+TextScriptImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Runtime.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 47f6a277ab4db56449411028a575565a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 17 - 0
Runtime/BuildingEffectArea.cs

@@ -0,0 +1,17 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.Grids
+{
+    [System.Serializable]
+    public class BuildingEffectArea
+    {
+        [FoldoutGroup("@areaName")] public string areaName = "New Effect Area";
+        [FoldoutGroup("@areaName")] public Vector2Int originOffset = new Vector2Int(0,0);
+        [FoldoutGroup("@areaName")] public Vector2Int size = new Vector2Int(1,1);
+        [FoldoutGroup("@areaName")] public GameObject arrowPrefab;
+        [FoldoutGroup("@areaName")] public PlacedObjectType.Dir arrowDirection;
+    }
+}

+ 11 - 0
Runtime/BuildingEffectArea.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8cc1230178149c94db4a61ea0e72c061
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 61 - 0
Runtime/BuildingGhost.cs

@@ -0,0 +1,61 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using KairoEngine.Core;
+using Shapes;
+
+namespace KairoEngine.Grids
+{
+
+    public class BuildingGhost : MonoBehaviour {
+
+        private Transform visual;
+        private PlacedObjectType placedObjectTypeSO;
+
+        private void Start() {
+            RefreshVisual();
+            GenericEvents.StartListening("OnSelectedChanged", OnSelectedChanged);
+        }
+
+        private void OnDisable()
+        {
+            GenericEvents.StopListening("OnSelectedChanged", OnSelectedChanged);
+        }
+
+        private void OnSelectedChanged() {
+            RefreshVisual();
+        }
+
+        private void LateUpdate() {
+            Vector3 targetPosition = GridBuildingSystem.Instance.GetMouseWorldSnappedPosition();
+            //targetPosition.y = 1f;
+            transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * 15f);
+            transform.rotation = Quaternion.Lerp(transform.rotation, GridBuildingSystem.Instance.GetPlacedObjectRotation(), Time.deltaTime * 15f);
+            if(GridBuildingSystem.buildingToolAvailable) visual.gameObject.SetActive(true);
+            else visual.gameObject.SetActive(false);
+        }
+
+        private void RefreshVisual() {
+            if (visual != null) {
+                Destroy(visual.gameObject);
+                visual = null;
+            }
+            PlacedObjectType placedObjectTypeSO = GridBuildingSystem.Instance.GetPlacedObjectTypeSO();
+            if (placedObjectTypeSO != null) {
+                visual = Instantiate(placedObjectTypeSO.visual, Vector3.zero, Quaternion.identity);
+                visual.parent = transform;
+                visual.localPosition = Vector3.zero;
+                visual.localEulerAngles = Vector3.zero;
+                SetLayerRecursive(visual.gameObject, 11);
+            }
+        }
+
+        private void SetLayerRecursive(GameObject targetGameObject, int layer) {
+            targetGameObject.layer = layer;
+            foreach (Transform child in targetGameObject.transform) {
+                SetLayerRecursive(child.gameObject, layer);
+            }
+        }
+
+    }
+}

+ 11 - 0
Runtime/BuildingGhost.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 72cba4a9acc909a45b3a0292f5926d31
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 94 - 0
Runtime/BuildingSelectionUi.cs

@@ -0,0 +1,94 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+using TMPro;
+using KairoEngine.UI;
+using KairoEngine.UI.Tooltips;
+using KairoEngine.UI.InteractionHandler;
+using KairoEngine.GameTools.InteractionTools;
+
+namespace KairoEngine.Grids
+{
+    public class BuildingSelectionUi : MonoBehaviour, IClickHandler
+    {
+        public bool generateOnStart = true;
+        public GridBuildingSystem gridBuildingSystem;
+        public Sprite destroyToolIcon;
+
+        public MenuUI menuUI;
+        public string tooltipType = "";
+
+        private void Start()
+        {
+            if(generateOnStart) CreateMenu();
+            
+        }
+
+        public void CreateMenu()
+        {
+            menuUI.DestroyMenu();
+            CreateMenuData();
+            menuUI.CreateMenu();
+        }
+
+        public void OnClick(string title)
+        {
+            for (int i = 0; i < gridBuildingSystem.placeObjectTypeList.Count; i++)
+            {
+                var obj = gridBuildingSystem.placeObjectTypeList[i];
+                if(obj.title == title) 
+                {
+                    InteractionToolsManager.instance.ChangeTool(1);
+                    gridBuildingSystem.ChangeSelectedPlacebleObject(i);
+                    return;
+                }
+            }
+            if(title == "Destroy") InteractionToolsManager.instance.ChangeTool(2);
+        }
+
+        private void CreateMenuData()
+        {
+            menuUI.buttons.Clear();
+            List<string> categories = new List<string>();
+            for (int i = 0; i < gridBuildingSystem.placeObjectTypeList.Count; i++)
+            {
+                bool duplicate = false;
+                for (int a = 0; a < categories.Count; a++)
+                {
+                    if(categories[a] == gridBuildingSystem.placeObjectTypeList[i].category) duplicate = true;
+                }
+                if(duplicate == false) categories.Add(gridBuildingSystem.placeObjectTypeList[i].category);
+            }
+            for (int i = 0; i < categories.Count; i++)
+            {
+                var data = new MenuButtomData();
+                data.title = categories[i];
+                data.subMenuParent = true;
+                //menuUI.buttons.Add(data);
+            }
+            for (int i = 0; i < gridBuildingSystem.placeObjectTypeList.Count; i++)
+            {
+                var obj = gridBuildingSystem.placeObjectTypeList[i];
+                var data = new MenuButtomData();
+                data.title = obj.title;
+                data.parent = obj.category;
+                data.description = obj.description;
+                data.action = obj.title;
+                data.graphic = obj.icon;
+                data.subMenuParent = false;
+                data.showTooltip = true;
+                data.tooltipType = tooltipType;
+                menuUI.buttons.Add(data);
+            }
+            var destroyData = new MenuButtomData();
+            destroyData.title = "Demolish";
+            destroyData.description = "Tool for destroying constructed buildings";
+            destroyData.action = "Destroy";
+            destroyData.graphic = destroyToolIcon;
+            destroyData.subMenuParent = false;
+            menuUI.buttons.Add(destroyData);
+        }
+    }
+}
+

+ 11 - 0
Runtime/BuildingSelectionUi.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 0b83e4c0bb4e52a4a905c8f6abab468e
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 241 - 0
Runtime/Grid.cs

@@ -0,0 +1,241 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Utils = KairoEngine.Utility.Utilities;
+using TMPro;
+
+namespace KairoEngine.Grids
+{
+    public enum GridPlane
+    {
+        XY,
+        XZ,
+        YZ
+    }
+
+    public class Grid<TGridObject>
+    {
+        
+
+        private int width;
+        private int height;
+        private float cellSize;
+        private Vector3 originPosition;
+        private bool debug = false;
+
+        private GridPlane plane = GridPlane.XY;
+
+        private TGridObject[,] gridArray;
+        private TextMesh[,] debugTextArray;
+
+        public event EventHandler<OnGridObjectChangedEventArgs> OnGridObjectChanged;
+        public class OnGridObjectChangedEventArgs : EventArgs 
+        {
+            public int x;
+            public int y;
+        }
+
+        public Grid(int width, int height, float cellSize, Vector3 originPosition, Func<Grid<TGridObject>, int, int, TGridObject> createGridObject, 
+            GridPlane plane = GridPlane.XY, bool debug = false)
+        {
+            this.width = width;
+            this.height = height;
+            this.cellSize = cellSize;
+            this.originPosition = originPosition;
+            this.gridArray = new TGridObject[width, height];
+            this.debug = debug;
+            this.plane = plane;
+
+            for (int x = 0; x < gridArray.GetLength(0); x++)
+            {
+                for (int y = 0; y < gridArray.GetLength(1); y++)
+                {
+                    gridArray[x, y] = createGridObject(this, x, y);
+                }
+            }
+            UpdateDebugView();
+        }
+
+        public int GetWidth() => width;
+        public int GetHeight() => height;
+        public float GetCellSize() => cellSize;
+
+        public Vector3 GetWorldPosition(int x, int y)
+        {
+            switch(plane)
+            {
+                case GridPlane.XY:
+                    return new Vector3(x, y, 0) * cellSize + originPosition;
+                case GridPlane.XZ:
+                    return new Vector3(x, 0, y) * cellSize + originPosition;
+                case GridPlane.YZ:
+                    return new Vector3(0, x, y) * cellSize + originPosition;
+                default:
+                    return new Vector3();
+            }
+        }
+
+        public void GetGridPosition(Vector3 worldPosition, out int x, out int y)
+        {
+            x = 0;
+            y = 0;
+            switch(plane)
+            {
+                case GridPlane.XY:
+                    x = Mathf.FloorToInt((worldPosition - originPosition).x/ cellSize);
+                    y = Mathf.FloorToInt((worldPosition - originPosition).y / cellSize);
+                    break;
+                case GridPlane.XZ:
+                    x = Mathf.FloorToInt((worldPosition - originPosition).x/ cellSize);
+                    y = Mathf.FloorToInt((worldPosition - originPosition).z / cellSize);
+                    break;
+                case GridPlane.YZ:
+                    x = Mathf.FloorToInt((worldPosition - originPosition).y/ cellSize);
+                    y = Mathf.FloorToInt((worldPosition - originPosition).z / cellSize);
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        public bool IsPositionInsideGrid(Vector2Int gridPosition)
+        {
+            if(gridPosition.x < 0) return false;
+            if(gridPosition.x > width - 1) return false;
+            if(gridPosition.y < 0) return false;
+            if(gridPosition.y > height - 1) return false;
+            return true;
+        }
+
+        public bool IsPositionInsideGrid(Vector3 worldPosition)
+        {
+            float maxWidth, maxHeight;
+            switch(plane)
+            {
+                case GridPlane.XY:
+                    maxWidth = originPosition.x + (cellSize * width);
+                    maxHeight = originPosition.y + (cellSize * height);
+                    if(worldPosition.x > maxWidth) return false;
+                    if(worldPosition.x < originPosition.x) return false;
+                    if(worldPosition.y > maxHeight) return false;
+                    if(worldPosition.y < originPosition.y) return false;
+                    break;
+                case GridPlane.XZ:
+                    maxWidth = originPosition.x + (cellSize * width);
+                    maxHeight = originPosition.z + (cellSize * height);
+                    if(worldPosition.x > maxWidth) return false;
+                    if(worldPosition.x < originPosition.x) return false;
+                    if(worldPosition.z > maxHeight) return false;
+                    if(worldPosition.z < originPosition.z) return false;
+                    break;
+                case GridPlane.YZ:
+                    maxWidth = originPosition.y + (cellSize * width);
+                    maxHeight = originPosition.z + (cellSize * height);
+                    if(worldPosition.y > maxWidth) return false;
+                    if(worldPosition.y < originPosition.y) return false;
+                    if(worldPosition.z > maxHeight) return false;
+                    if(worldPosition.z < originPosition.z) return false;
+                    break;
+                default:
+                    break;
+            }
+            return true;
+        }
+
+        public void SetObject(int x, int y, TGridObject value)
+        {
+            if(x >= 0 && y >= 0 && x < width && y < height)
+            {
+                gridArray[x, y] = value;
+                if(debug) debugTextArray[x, y].text = gridArray[x, y].ToString();
+                TriggerGridObjectChanged(x, y);
+            }
+        }
+
+        public void SetObject(Vector3 worldPosition, TGridObject value)
+        {
+            int x, y;
+            GetGridPosition(worldPosition, out x, out y);
+            SetObject(x, y, value);
+        }
+
+        public TGridObject GetObject(int x, int y)
+        {
+            if(x >= 0 && y >= 0 && x < width && y < height)
+            {
+                return gridArray[x, y];
+            }
+            else return default(TGridObject);
+        }
+
+        public TGridObject GetObject(Vector3 worldPosition)
+        {
+            int x, y;
+            GetGridPosition(worldPosition, out x, out y);
+            return GetObject(x, y);
+        } 
+
+        public Vector3 GetQuadSize()
+        {
+            switch(plane)
+            {
+                case GridPlane.XY:
+                    return new Vector3(1, 1, 0) * GetCellSize();
+                case GridPlane.XZ:
+                    return new Vector3(1, 0, 1) * GetCellSize();
+                case GridPlane.YZ:
+                    return new Vector3(0, 1, 1) * GetCellSize();
+                default:
+                    return new Vector3(0, 0, 0);
+            }
+        }
+
+        public GridPlane GetGridPlane() => plane;
+
+        public void TriggerGridObjectChanged(int x, int y)
+        {
+            if(OnGridObjectChanged != null) OnGridObjectChanged(this, new OnGridObjectChangedEventArgs {x = x, y = y});
+            UpdateDebugView();
+        }
+
+        private void UpdateDebugView()
+        {
+            DestroyDebugText();
+            if(debug)
+            {
+                this.debugTextArray = new TextMesh[width, height];
+
+                for (int x = 0; x < gridArray.GetLength(0); x++)
+                {
+                    for (int y = 0; y < gridArray.GetLength(1); y++)
+                    {
+                        debugTextArray[x, y] = Utils.CreateWorldText(gridArray[x,y]?.ToString(), null, GetWorldPosition(x, y) + GetQuadSize() * .5f, 
+                            12, Color.white, TextAnchor.MiddleCenter);
+                        Debug.DrawLine(GetWorldPosition(x, y), GetWorldPosition(x, y + 1), Color.white, 100f);
+                        Debug.DrawLine(GetWorldPosition(x, y), GetWorldPosition(x + 1, y), Color.white, 100f);
+                    }
+                }
+                Debug.DrawLine(GetWorldPosition(0, height), GetWorldPosition(width, height), Color.white, 100f);
+                Debug.DrawLine(GetWorldPosition(width, 0), GetWorldPosition(width, height), Color.white, 100f);
+            }
+        }
+
+        private void DestroyDebugText()
+        {
+            if(this.debugTextArray != null)
+            {
+                for (int x = 0; x < this.debugTextArray.GetLength(0); x++)
+                {
+                    for (int y = 0; y < this.debugTextArray.GetLength(1); y++)
+                    {
+                        GameObject.Destroy(debugTextArray[x,y].gameObject);
+                    }
+                }
+            }
+        }
+    }
+
+    
+}
+

+ 11 - 0
Runtime/Grid.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5686bf3241cfc4e48b981af0485c5b9d
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 402 - 0
Runtime/GridBuildingSystem.cs

@@ -0,0 +1,402 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using KairoEngine.Utility;
+using KairoEngine.Core;
+using KairoEngine.UI;
+using KairoEngine.Stockpiles;
+using Utils = KairoEngine.Utility.Utilities;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.Grids
+{
+    [HideMonoScript]
+    public class GridBuildingSystem : MonoBehaviour
+    {
+        public static GridBuildingSystem Instance;
+
+        public Vector2Int gridSize = new Vector2Int(8,8);
+        public float cellSize = 10f;
+        public Vector3 gridPosition = new Vector3();
+        public bool debug = false;
+        public string stockpileOwner = "Player";
+        public bool enableBuildingPlacement = true;
+        public bool enableBuildingDestruction = false;
+        [SerializeField] private Grid<GridObject> grid;
+
+        //[OnInspectorInit("CreateGrid")]
+        public List<PlacedObjectType> placeObjectTypeList;
+        private PlacedObjectType placedObjectType;
+        private PlacedObjectType.Dir dir = PlacedObjectType.Dir.Down;
+         public List<PlacedObject> placedObjects = new List<PlacedObject>();
+
+        public LayerMask layerMask;
+
+        public static bool buildingToolAvailable 
+        {
+            get;
+            private set;
+        }
+
+        public static bool buildingDestructionToolAvailable 
+        {
+            get;
+            private set;
+        }
+        
+        void Awake()
+        {
+            CreateGrid();
+            placedObjectType = placeObjectTypeList[0];
+            if(Instance == null) Instance = this;
+            else Destroy(gameObject);
+        }
+
+        void Update()
+        {
+            Vector3 worldPos = Utils.GetMouseWorldPosition(layerMask.value);
+            if(!MouseInputUIBlocker.BlockedByUI && grid.IsPositionInsideGrid(worldPos))
+            {
+                if(enableBuildingPlacement)
+                {
+                    buildingToolAvailable = true;
+                    if(Input.GetKeyDown(KeyCode.R)) RotatePlacebableObject();
+                    if(Input.GetMouseButtonDown(0)) PlaceObjectInGrid(worldPos);
+                }
+                else buildingToolAvailable = false;
+                if(enableBuildingDestruction)
+                {
+                    buildingDestructionToolAvailable = true;
+                    if(Input.GetMouseButtonDown(0)) RemoveObjectFromGrid(worldPos);
+                }
+                else buildingDestructionToolAvailable = false;
+            }
+        }
+
+        public bool IsGridCreated() => grid != null;
+
+        public void CreateGrid()
+        {
+            if(grid == null)
+            {
+                grid = new Grid<GridObject>(gridSize.x, gridSize.y, cellSize, gridPosition, 
+                    (Grid<GridObject> g, int x, int y) => new GridObject(g, x, 0, y), GridPlane.XZ, debug);
+            }
+        }
+
+        public void ClearGrid() => grid = null;
+
+        private void DeselectObjectType() 
+        {
+            placedObjectType = null; 
+            RefreshSelectedObjectType();
+        }
+
+        private void RefreshSelectedObjectType() => GenericEvents.Trigger("OnSelectedChanged");
+
+        public Vector2Int GetGridPosition(Vector3 worldPosition) {
+            if(grid == null) return new Vector2Int();
+            grid.GetGridPosition(worldPosition, out int x, out int z);
+            return new Vector2Int(x, z);
+        }
+
+        public Vector3 GetPositionFromGrid(int x, int y) => grid != null ? grid.GetWorldPosition(x, y) : new Vector3();
+
+        public Vector3 GetGridPositionOffset() => gridPosition - transform.position;
+
+        public PlacedObjectType.Dir GetDir() => dir;
+
+        public Vector3 GetCellSize()
+        {
+            if(grid == null) return new Vector3(cellSize, cellSize, cellSize);
+            switch(grid.GetGridPlane())
+            {
+                default:
+                case GridPlane.XY:
+                    return new Vector3(cellSize, cellSize, 0);
+                case GridPlane.XZ:
+                    return new Vector3(cellSize, 0, cellSize);
+                case GridPlane.YZ:
+                    return new Vector3(0, cellSize, cellSize);
+            }
+        }
+
+        public Vector3 GetMouseWorldSnappedPosition() {
+            Vector3 mousePosition = Utils.GetMouseWorldPosition(layerMask.value);
+            if(grid == null) return new Vector3();
+            grid.GetGridPosition(mousePosition, out int x, out int z);
+            if (placedObjectType != null) {
+                Vector2Int rotationOffset = placedObjectType.GetRotationOffset(dir);
+                Vector3 placedObjectWorldPosition = grid.GetWorldPosition(x, z) + new Vector3(rotationOffset.x, 0, rotationOffset.y) * grid.GetCellSize();
+                return placedObjectWorldPosition;
+            } else {
+                return mousePosition;
+            }
+        }
+
+        public Quaternion GetPlacedObjectRotation() {
+            if (placedObjectType != null) {
+                return Quaternion.Euler(0, placedObjectType.GetRotationAngle(dir), 0);
+            } else {
+                return Quaternion.identity;
+            }
+        }
+
+        public PlacedObjectType GetPlacedObjectTypeSO() => placedObjectType;
+
+        public bool CanPlaceObject(int x, int z) => placedObjectType.CanPlaceObject(grid, x, z, dir);
+
+        public void PlaceObjectInGrid(Vector3 worldPos)
+        {
+            grid.GetGridPosition(Utils.GetMouseWorldPosition(layerMask.value), out int x, out int z);
+            PlaceObjectInGrid(x, z);
+        }
+
+        public void PlaceObjectInGrid(int x, int z)
+        {
+            PlaceObjectInGrid(x, z, placedObjectType, dir);
+        }
+
+        public void PlaceObjectInGrid(int x, int z, PlacedObjectType placedObjectType, PlacedObjectType.Dir dir, bool checkResources = true, 
+            bool checkPlace = true, bool playSound = true, bool destructable = true)
+        {
+            Vector3 worldPos = grid.GetWorldPosition(x, z);
+            if(CanPlaceObject(x, z) || checkPlace == false)
+            {
+                if(HasResources() || checkResources == false)
+                {
+                    Vector2Int rotationOffset = placedObjectType.GetRotationOffset(dir);
+                    Vector3 pos = grid.GetWorldPosition(x + rotationOffset.x, z + rotationOffset.y);
+                    PlacedObject placedObject = PlacedObject.Create(pos, new Vector2Int(x, z), dir, placedObjectType, playSound);
+                    placedObject.destructable = destructable;
+                    placedObjects.Add(placedObject);
+                    List<Vector2Int> gridPositionList = placedObjectType.GetGridPositionList(new Vector2Int(x, z), dir);
+                    foreach (var gridPosition in gridPositionList)
+                    {
+                        grid.GetObject(gridPosition.x, gridPosition.y).SetPlacedObject(placedObject); 
+                    }
+                    if(checkResources) 
+                    {
+                        GenericEvents.Trigger("BuildingPlaced", placedObjectType.title);
+                        StockpileManager.instance.RemoveResources(stockpileOwner, placedObjectType.cost);
+                        for (int i = 0; i < placedObjectType.cost.Count; i++)
+                        {
+                            var stock = placedObjectType.cost[i];
+                            float delay = 350f * i;
+                            Timer.Execute(delay, () => {
+                                string resource = stock.stockpileType.title.ToLower();
+                                if(resource == "mineral") resource = "minerals";
+                                string message = $"- {stock.ammount} <sprite name={resource}>";
+                                FloatingMessageEvents.Send(message, GetCurrentPlacebleObjectCenter(pos), 5, 1.3f, 20f);
+                            });
+                        }
+                    }
+                }
+                else
+                {
+                    Vector3 finalPos = GetCurrentPlacebleObjectCenter(worldPos);
+                    DrawResourceErrorMessage(finalPos);
+                    GenericEvents.Trigger("CannotBuildError");
+                }
+            }
+            else
+            {
+                Vector3 finalPos = new Vector3(worldPos.x, worldPos.y + 10, worldPos.z);
+                DrawRulesetErrorMessage(finalPos, x, z);
+                GenericEvents.Trigger("CannotBuildError");
+            }
+        }
+
+        public void RemoveObjectFromGrid(Vector3 worldPos)
+        {
+            GridObject gridObject = grid.GetObject(worldPos);
+            RemoveObjectFromGrid(gridObject);
+        }
+
+        public void RemoveObjectFromGrid(int x, int z)
+        {
+            GridObject gridObject = grid.GetObject(x, z);
+            RemoveObjectFromGrid(gridObject);
+        }
+
+        public void RemoveObjectFromGrid(GridObject gridObject)
+        {
+            if(gridObject == null) return;
+            PlacedObject placedObject = gridObject.GetPlacedObject();
+            if(placedObject != null)
+            {
+                Vector3 worldPos = grid.GetWorldPosition(gridObject.GetPosition().x, gridObject.GetPosition().z);
+                Vector3 finalPos = GetCurrentPlacebleObjectCenter(worldPos);
+                if(!placedObject.placedObjectType.isDestructable) 
+                {
+                    FloatingMessageEvents.Send("Indestructable!", finalPos, 5, 2f, 20f);
+                    return;
+                }
+                if(!placedObject.destructable) 
+                {
+                    FloatingMessageEvents.Send("Cannot remove initial building", finalPos, 5, 2f, 20f);
+                    return;
+                }
+                GenericEvents.Trigger("BuildingRemoved", placedObject.placedObjectType.title, worldPos);
+                placedObjects.Remove(placedObject);
+                placedObject.DestroySelf();
+                List<Vector2Int> gridPositionList = placedObject.GetGridPositionList();
+                foreach (var gridPosition in gridPositionList)
+                {
+                    grid.GetObject(gridPosition.x, gridPosition.y).ClearPlacedObject(); 
+                }
+                
+            }
+        }
+
+        public void RotatePlacebableObject()
+        {
+            dir = PlacedObjectType.GetNextDir(dir);
+            Vector3 pos = Utils.GetMouseWorldPosition(layerMask.value);
+            Vector3 finalPos = new Vector3(pos.x, pos.y + 10, pos.z);
+            //Utilities.CreateWorldTextPopup(null, "" + dir, pos, 18, Color.white, finalPos, 1.2f, true);
+            FloatingMessageEvents.Send("" + dir, GetCurrentPlacebleObjectCenter(pos), 5, 1.2f, 20f);
+        }
+
+        public void ChangeSelectedPlacebleObject(int index)
+        {
+            placedObjectType = placeObjectTypeList[index];
+            RefreshSelectedObjectType();
+        }
+
+        public void ChangeSelectedPlacebleObject(PlacedObjectType newPlacedObjectType)
+        {
+            placedObjectType = newPlacedObjectType;
+            RefreshSelectedObjectType();
+        }
+
+        public bool IsPositionInsideGrid(Vector2Int gridPos)
+        {
+            if(grid == null) return false;
+            return grid.IsPositionInsideGrid(gridPos);
+        }
+
+        public bool IsPositionInsideGrid(Vector3 worldPos)
+        {
+            if(grid == null) return false;
+            return grid.IsPositionInsideGrid(worldPos);
+        }
+
+        public PlacedObject GetPlacedObjectInGrid(Vector2Int gridPosition)
+        {
+            if(grid == null) return null;
+            GridObject gridObject = grid.GetObject(gridPosition.x, gridPosition.y);
+            if(gridObject == null) return null;
+            PlacedObject placedObject = gridObject.GetPlacedObject();
+            return placedObject;
+        }
+
+        public bool HasResources() => StockpileManager.instance.HasResources(stockpileOwner, placedObjectType.cost);
+        public bool HasResource(KairoEngine.Stockpiles.Stockpile stockpile) => StockpileManager.instance.HasResource(stockpileOwner, stockpile);
+
+        public void RemovePlacedObjects()
+        {
+            for (int i = 0; i < placedObjects.Count; i++)
+            {
+                Destroy(placedObjects[i].gameObject);
+            }
+            placedObjects.Clear();
+        }
+
+        public Vector3 GetCurrentPlacebleObjectCenter(Vector3 worldPos)
+        {
+            if(dir == PlacedObjectType.Dir.Down || dir == PlacedObjectType.Dir.Up)
+            {
+                return new Vector3(
+                    worldPos.x + (placedObjectType.width * cellSize)/2, 
+                    worldPos.y + 1, 
+                    worldPos.z + (placedObjectType.height * cellSize)/2);
+            }
+            else
+            {
+                return new Vector3(
+                    worldPos.x + (placedObjectType.height * cellSize)/2, 
+                    worldPos.y + 1, 
+                    worldPos.z + (placedObjectType.width * cellSize)/2);
+            }
+            
+        }
+
+        private void DrawResourceErrorMessage(Vector3 finalPos)
+        {
+            List<string> sprites = new List<string>();
+            bool missingResources = false;
+            for (int i = 0; i <  placedObjectType.cost.Count; i++)
+            {
+                if(!HasResource(placedObjectType.cost[i]))
+                {
+                    sprites.Add(placedObjectType.cost[i].stockpileType.spriteText);
+                    missingResources = true;
+                }
+            }
+            if(missingResources == false) return;
+            switch (sprites.Count)
+            {
+                case 1:
+                    FloatingMessageEvents.Send($"Not enought {sprites[0]}", finalPos, 5, 1.5f, 20f);
+                    break;
+                case 2:
+                    FloatingMessageEvents.Send($"Not enought {sprites[0]} and {sprites[1]}", finalPos, 5, 1.5f, 20f);
+                    break;
+                case 3:
+                    FloatingMessageEvents.Send($"Not enought {sprites[0]}, {sprites[1]} and {sprites[2]}", finalPos, 5, 1.5f, 20f);
+                    break;
+                case 4:
+                    FloatingMessageEvents.Send($"Not enought {sprites[0]}, {sprites[1]}, {sprites[2]} and {sprites[3]}", finalPos, 5, 1.5f, 20f);
+                    break;
+                default:
+                    FloatingMessageEvents.Send("Not enought resources", finalPos, 5, 2f, 20f);
+                    break;
+            }
+        }
+
+        private void DrawRulesetErrorMessage(Vector3 finalPos, int x, int y)
+        {
+            List<PlaceableObjectRule> brokenRules = placedObjectType.GetRulesetError(grid, x, y, dir);
+            for (int i = 0; i < brokenRules.Count; i++)
+            {
+                string message = "";
+                switch (brokenRules[i].GetRule())
+                {
+                    case PlaceableObjectRule.Rule.EmptyCell:
+                        message = $"Area must be empty!";
+                        break;
+                    case PlaceableObjectRule.Rule.Connection:
+                        message = $"Invalid connection";
+                        //message += $" of type {brokenRules[i].GetConnector()}!";
+                        break;
+                    case PlaceableObjectRule.Rule.CellOcupied:
+                        message = $"Must be placed on a {brokenRules[i].GetTargetObject().title}";
+                        break;
+                    case PlaceableObjectRule.Rule.AdjacentCellOcupied:
+                        message = $"Adjacent cell need to be of type {brokenRules[i].GetTargetObject().title}";
+                        break;
+                    case PlaceableObjectRule.Rule.AdjacentEmptyCell:
+                        message = $"Adjacent cell must be empty";
+                        break;
+                    case PlaceableObjectRule.Rule.CellInRadius:
+                        message = $"No {brokenRules[i].GetTargetObject().title} in {brokenRules[i].GetRadius()} radius";
+                        break;
+                    case PlaceableObjectRule.Rule.EmptyArea:
+                        message = $"Area must be empty!";
+                        break;
+                    default:
+                        break;
+                }
+                Timer.Execute(350f * i, () => {
+                    FloatingMessageEvents.Send(message, finalPos, 5, 2f, 20f);
+                });
+            }
+            Timer.Execute(350f * brokenRules.Count, () => {
+                DrawResourceErrorMessage(finalPos);
+            });
+            
+        }
+
+    }
+}

+ 11 - 0
Runtime/GridBuildingSystem.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ead5e8336c519844aa2fce002d81ecfb
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 40 - 0
Runtime/GridObject.cs

@@ -0,0 +1,40 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace KairoEngine.Grids
+{
+    public class GridObject
+    {
+        private Grid<GridObject> grid;
+        private int x;
+        private int y;
+        private int z;
+
+        private PlacedObject placedObject;
+
+        public GridObject(Grid<GridObject> grid, int x, int y, int z)
+        {
+            this.grid = grid;
+            this.x = x;
+            this.y = y;
+            this.z = z;
+        }
+
+        public PlacedObject GetPlacedObject() => this.placedObject;
+
+        public void SetPlacedObject(PlacedObject placedObject)
+        {
+            this.placedObject = placedObject;
+            grid.TriggerGridObjectChanged(x, z);
+        } 
+
+        public void ClearPlacedObject() => this.placedObject = null;
+
+        public bool CanBuild() => this.placedObject == null;
+
+        public override string ToString() => $"{x}, {z}";
+
+        public Vector3Int GetPosition() => new Vector3Int(x, y, z);
+    }
+}

+ 11 - 0
Runtime/GridObject.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5794538aa9f87f94dab108b62ceff8dc
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 44 - 0
Runtime/GridTest.cs

@@ -0,0 +1,44 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using KairoEngine.Grids;
+using KairoEngine.Utility;
+
+public class GridTest : MonoBehaviour
+{
+    public bool debugGrid = false;
+    public Vector2Int gridSize;
+    public float cellSize = 10f;
+
+    public GridPlane gridPlane = GridPlane.XY;
+    public Vector3 gridPosition = new Vector3();
+    private Grid<HeatMapGridObject> grid;
+
+
+    [SerializeField] private HeatMapVisual heatMapVisual;
+
+    void Start()
+    {
+        grid = new Grid<HeatMapGridObject>(gridSize.x, gridSize.y, cellSize, gridPosition, (Grid<HeatMapGridObject> g, int x, int y) => new HeatMapGridObject(g, x, y), 
+            gridPlane, debugGrid);
+        heatMapVisual.SetGrid(grid);
+    }
+
+    private void Update()
+    {
+        if(Input.GetMouseButtonDown(0))
+        {
+            Vector3 mousePos = Utilities.GetMouseWorldPosition();
+            //int value = grid.GetValue(mousePos);
+            //value += 10;
+            //value = Mathf.Clamp(value, HeatMapVisual.HEAT_MAP_MIN_VALUE, HeatMapVisual.HEAT_MAP_MAX_VALUE);
+            //grid.SetValue(Utilities.GetMouseWorldPosition(), value);
+            heatMapVisual.AddValue(mousePos, 100, 0, 5);
+        }
+
+        if(Input.GetMouseButtonDown(1))
+        {
+            Debug.Log(grid.GetObject(Utilities.GetMouseWorldPosition()));
+        }
+    }
+}

+ 11 - 0
Runtime/GridTest.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 114d1ea4c73a63a46a3625874e1661ab
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 120 - 0
Runtime/HeatMapVisual.cs

@@ -0,0 +1,120 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using KairoEngine.Utility;
+
+namespace KairoEngine.Grids
+{
+    public class HeatMapVisual : MonoBehaviour
+    {
+        private Grid<HeatMapGridObject> grid;
+        private Mesh mesh;
+
+        private bool updateMesh = false;
+        
+        private void Awake()
+        {
+            mesh = new Mesh();
+            GetComponent<MeshFilter>().mesh = mesh;
+        }
+
+        public void SetGrid(Grid<HeatMapGridObject> grid) 
+        {
+            this.grid = grid;
+            UpdateHeatMapVisual();
+            grid.OnGridObjectChanged += Grid_OnGridObjectChanged;
+        }
+
+        private void UpdateHeatMapVisual()
+        {
+            MeshUtils.CreateEmptyMeshArrays(grid.GetWidth() * grid.GetHeight(), out Vector3[] vertices, out Vector2[] uv, out int[] triangles);
+            for (int x = 0; x < grid.GetWidth(); x++)
+            {
+                for (int y = 0; y < grid.GetHeight(); y++)
+                {
+                    int index = x * grid.GetHeight() + y;
+                    Vector3 quadSize = grid.GetQuadSize();
+                    HeatMapGridObject gridObject = grid.GetObject(x, y);
+                    Vector2 gridValueUV = new Vector2(gridObject.GetValueNormalized(), 0f);
+                    Vector3 pos = grid.GetWorldPosition(x, y) + (quadSize * 0.5f);
+                    float rot = 0f;
+                    MeshUtils.AddToMeshArrays(vertices, uv, triangles, index, pos, rot, quadSize, gridValueUV, gridValueUV);
+                }
+            }
+            mesh.vertices = vertices;
+            mesh.uv = uv;
+            mesh.triangles = triangles;
+        }
+
+        private void Grid_OnGridObjectChanged(object sender, Grid<HeatMapGridObject>.OnGridObjectChangedEventArgs e) => updateMesh = true;
+
+        private void LateUpdate()
+        {
+            if(updateMesh)
+            {
+                updateMesh = false;
+                Debug.Log("Grid event fired");
+                UpdateHeatMapVisual();
+            }
+        }
+
+        public void AddValue(int x, int y, int value)
+        {
+            HeatMapGridObject obj =  grid.GetObject(x, y);
+            if(obj != null)
+            {
+                obj.AddValue(value);
+                //grid.SetObject(x, y, obj);
+            }
+        }
+
+        public void AddValue(Vector3 worldPosition, int value, int fullValueRange, int totalRange)
+        {
+            int lowerValueAmmount = Mathf.RoundToInt((float)value / (totalRange - fullValueRange));
+            grid.GetGridPosition(worldPosition, out int originX, out int originY);
+            for (int x = 0; x < totalRange; x++)
+            {
+                for (int y = 0; y < totalRange - x; y++)
+                {
+                    int radius = x + y;
+                    int addValueAmmount = value;
+                    if(radius > fullValueRange) addValueAmmount -= lowerValueAmmount * (radius - fullValueRange);
+                    AddValue(originX + x, originY + y, addValueAmmount);
+                    if(x != 0) AddValue(originX - x, originY + y, addValueAmmount);
+                    if(y != 0) AddValue(originX + x, originY - y, addValueAmmount);
+                    if(y != 0 && x != 0) AddValue(originX - x, originY - y, addValueAmmount);
+                }
+            }
+        }
+    }
+
+    public class HeatMapGridObject 
+    {
+        private const int MIN = 0;
+        private const int MAX = 100;
+        private Grid<HeatMapGridObject> grid;
+        private int x;
+        private int y;
+        public int value;
+
+        public HeatMapGridObject(Grid<HeatMapGridObject> grid, int x, int y)
+        {
+            this.grid = grid;
+            this.x = x;
+            this.y = y;
+        }
+
+        public void AddValue(int addValue)
+        {
+            value += addValue;
+            value = Mathf.Clamp(value, MIN, MAX);
+            grid.TriggerGridObjectChanged(x, y);
+        }
+
+        public float GetValueNormalized() => (float)value / MAX;
+
+        public override string ToString() => value.ToString();
+
+    }
+
+}

+ 11 - 0
Runtime/HeatMapVisual.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: d2cc0560248bb344991e7de2cf173fe0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 25 - 0
Runtime/KairoEngine.Grids.asmdef

@@ -0,0 +1,25 @@
+{
+    "name": "KairoEngine.Grids",
+    "rootNamespace": "",
+    "references": [
+        "GUID:7e5ae6a38d1532248b4c890eca668b06",
+        "GUID:165d83fc3bb2a4144925c85421871d8e",
+        "GUID:142285d3db5e7e849b02ea3a75bc2de7",
+        "GUID:b5398ac4f2218bd4ba186b0baff7fb21",
+        "GUID:f452697229e6bcc469c0eff1574ac090",
+        "GUID:d5cdde771ecbd5e47bdbe207903a3b3c",
+        "GUID:6055be8ebefd69e48b49212b09b47b2f",
+        "GUID:5f03fc37b95cb644599751ca563336b2",
+        "GUID:13a6fdc14ca9df84eb8019bc66fa9e5d",
+        "GUID:560b04d1a97f54a4e82edc0cbbb69285"
+    ],
+    "includePlatforms": [],
+    "excludePlatforms": [],
+    "allowUnsafeCode": false,
+    "overrideReferences": false,
+    "precompiledReferences": [],
+    "autoReferenced": true,
+    "defineConstraints": [],
+    "versionDefines": [],
+    "noEngineReferences": false
+}

+ 7 - 0
Runtime/KairoEngine.Grids.asmdef.meta

@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 492b4aa2d50127f4281e1d2833b7017c
+AssemblyDefinitionImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 16 - 0
Runtime/PlaceableObjectConnector.cs

@@ -0,0 +1,16 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.Grids
+{
+    [System.Serializable]
+    public class PlaceableObjectConnector
+    {
+        [HideLabel, HorizontalGroup("connector", 0.4f)] public string title = "new connector";
+        [HideLabel, HorizontalGroup("connector", 0.3f)] public Vector2Int position;
+        [HideLabel, HorizontalGroup("connector", 0.3f)] public PlacedObjectType.Dir dir;
+    }
+}
+

+ 11 - 0
Runtime/PlaceableObjectConnector.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 01f781039dbad634bad63d7418b51e27
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 18 - 0
Runtime/PlaceableObjectData.cs

@@ -0,0 +1,18 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.Grids
+{
+    [System.Serializable]
+    public class PlaceableObjectData
+    {
+        [HideLabel, HorizontalGroup("connector", 0.03f)] public bool destructable = true;
+        [HideLabel, HorizontalGroup("connector", 0.52f)] public PlacedObjectType building;
+        [HideLabel, HorizontalGroup("connector", 0.15f)] public Vector2Int position;
+        [HideLabel, HorizontalGroup("connector", 0.3f)] public PlacedObjectType.Dir dir;
+        
+    }
+}
+

+ 11 - 0
Runtime/PlaceableObjectData.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3114dc337bc57a545a3566146354f4c0
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 138 - 0
Runtime/PlaceableObjectRule.cs

@@ -0,0 +1,138 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Sirenix.OdinInspector;
+
+namespace KairoEngine.Grids
+{
+    [System.Serializable]
+    public class PlaceableObjectRule
+    {
+        public enum Rule
+        {
+            EmptyCell,
+            Connection,
+            CellOcupied,
+            AdjacentCellOcupied,
+            AdjacentEmptyCell,
+            CellInRadius,
+            EmptyArea
+        }
+
+        private string title 
+        {
+            get 
+            {
+                switch (rule)
+                {
+                    case Rule.EmptyCell:
+                        return $"Empty cell";
+                    case Rule.Connection:
+                        return $"Has connector";
+                    case Rule.CellOcupied:
+                        if(targetObject != null) return $"Cell ocupied by \"{targetObject?.title}\"";
+                        else return $"Cell ocupied (ERROR)";
+                    case Rule.AdjacentCellOcupied:
+                        if(targetObject != null) return $"{objDir.ToString()} cell ocupied by \"{targetObject?.title}\"";
+                        else return $"{objDir.ToString()} cell ocupied (ERROR)";
+                    case Rule.AdjacentEmptyCell:
+                        return $"{objDir.ToString()} cell is empty";
+                    case Rule.CellInRadius:
+                        if(targetObject != null) return $"\"{targetObject?.title}\" is in a {radius} cell radius";
+                        else return $"Cell in radius (ERROR)";
+                    case Rule.EmptyArea:
+                        return $"Empty cells in a {radius} cell radius";
+                    default:
+                        return "?";
+                }
+            }
+        }
+
+        [SerializeField, FoldoutGroup("@title")] private Rule rule = Rule.EmptyCell;
+
+        [ShowIf("@rule == Rule.CellOcupied || rule == Rule.AdjacentCellOcupied || rule == Rule.CellInRadius")]
+        [SerializeField, FoldoutGroup("@title")] private PlacedObjectType targetObject;
+
+        [ShowIf("@rule == Rule.AdjacentCellOcupied || rule == Rule.AdjacentEmptyCell")]
+        [SerializeField, FoldoutGroup("@title")] private PlacedObjectType.Dir objDir;
+
+        [ShowIf("@rule == Rule.CellInRadius || rule == Rule.EmptyArea")]
+        [SerializeField, FoldoutGroup("@title")] private int radius = 3;
+
+        [ShowIf("@rule == Rule.Connection")]
+        [SerializeField, FoldoutGroup("@title")] private string connector = "connector name";
+
+
+        public bool Evaluate(Grid<GridObject> grid, PlacedObjectType placedObjectType, int x, int y, PlacedObjectType.Dir dir)
+        {
+            switch (rule)
+            {
+                case Rule.EmptyCell:
+                    return IsEmptyCells(grid, placedObjectType, x, y, dir);
+                case Rule.Connection:
+                    return HasConnection(grid, placedObjectType, x, y, dir);
+                case Rule.CellOcupied:
+                    break;
+                case Rule.AdjacentCellOcupied:
+                    break;
+                case Rule.AdjacentEmptyCell:
+                    break;
+                case Rule.CellInRadius:
+                    break;
+                case Rule.EmptyArea:
+                    break;
+                default:
+                    break;
+            }
+            return false;
+        }
+
+        public Rule GetRule() => rule;
+        public PlacedObjectType GetTargetObject() => targetObject;
+        public int GetRadius() => radius;
+        public string GetConnector() => connector;
+
+        public bool IsEmptyCells(Grid<GridObject> grid, PlacedObjectType placedObjectType, int x, int z, PlacedObjectType.Dir dir)
+        {
+            GridObject gridObject = grid.GetObject(x, z);
+            if(gridObject == null) return false;
+            List<Vector2Int> gridPositionList = placedObjectType.GetGridPositionList(new Vector2Int(x, z), dir);
+            bool canBuild = true;
+            foreach (var gridPosition in gridPositionList)
+            {
+                var obj = grid.GetObject(gridPosition.x, gridPosition.y);
+                if(obj != null)
+                {
+                    if(!grid.GetObject(gridPosition.x, gridPosition.y).CanBuild()) canBuild = false;
+                }
+                else canBuild = false;
+            }
+            return canBuild;
+        }
+
+        public bool HasConnection(Grid<GridObject> grid, PlacedObjectType placedObjectType, int x, int z, PlacedObjectType.Dir dir)
+        {
+            for (int i = 0; i < placedObjectType.connectors.Count; i++)
+            {
+                Vector2Int pos = new Vector2Int(x, z);
+                pos = pos + placedObjectType.GetConnectorPositionOffset(i, dir);
+                Vector2Int targetPos = pos + placedObjectType.GetConnectorTargetPositionOffset(i, dir);
+                GridObject gridObject = grid.GetObject(targetPos.x, targetPos.y);
+                
+                if(gridObject == null) continue;
+                if(gridObject.GetPlacedObject() == null) continue;
+                if(gridObject.GetPlacedObject().HasConnection(placedObjectType.connectors[i].title, pos.x, pos.y)) 
+                {
+                    //Debug.Log($"Connector found from {pos} to {targetPos}");
+                    return true;
+                }
+                else
+                {
+                    //Debug.Log($"No connector found from {pos} to {targetPos}");
+                }
+            }
+            return false;
+        }
+    }
+}
+

+ 11 - 0
Runtime/PlaceableObjectRule.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f1093485db99d7d47840e6bf854eb496
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 40 - 0
Runtime/PlaceableObjectRuleGroup.cs

@@ -0,0 +1,40 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using KairoEngine.Core;
+
+namespace KairoEngine.Grids
+{
+    [System.Serializable]
+    public class PlaceableObjectRuleGroup
+    {
+        public ConditionType condition;
+        public List<PlaceableObjectRule> rules = new List<PlaceableObjectRule>();
+
+        public bool Evaluate(Grid<GridObject> grid, PlacedObjectType placedObjectType, int x, int y, PlacedObjectType.Dir dir)
+        {
+            List<bool> results = new List<bool>();
+            for (int i = 0; i < rules.Count; i++)
+            {
+                results.Add(rules[i].Evaluate(grid, placedObjectType, x, y, dir));
+            }
+            if(condition == ConditionType.AND)
+            {
+                foreach (var value in results)
+                {
+                    if(value == false) return false;
+                }
+                return true;
+            }
+            else
+            {
+                foreach (var value in results)
+                {
+                    if(value == true) return true;
+                }
+                return false;
+            }
+        }
+    }
+}
+

+ 11 - 0
Runtime/PlaceableObjectRuleGroup.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1de80d8573911f841b0e1d47bb6fd101
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 133 - 0
Runtime/PlacedObject.cs

@@ -0,0 +1,133 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using KairoEngine.Core;
+using KairoEngine.Core.GameActions;
+using KairoEngine.UI.Tooltips;
+using KairoEngine.GameTools.Selectables;
+using KairoEngine.SFX;
+
+namespace KairoEngine.Grids
+{
+    public class PlacedObject : MonoBehaviour, ISelectableObject
+    {
+        public PlacedObjectType placedObjectType { get; private set;}
+        public Vector2Int origin { get; private set;}
+        public PlacedObjectType.Dir dir { get; private set;}
+        public float destroyDelay = 3000f;
+        public bool destructable = true;
+
+        [SerializeField] private List<GameActionsController> actionsControllerList;
+        [SerializeField] private GameActionContext context;
+
+        public static PlacedObject Create(Vector3 worldPosition, Vector2Int origin, PlacedObjectType.Dir dir, 
+            PlacedObjectType placedObjectType, bool playCreatedSound = true)
+        {
+            Quaternion quaternion = Quaternion.Euler(0, placedObjectType.GetRotationAngle(dir), 0);
+            Transform placedObjectTransform = Instantiate(placedObjectType.prefab, worldPosition, quaternion);
+            PlacedObject placedObject = placedObjectTransform.GetComponent<PlacedObject>();
+            placedObject.placedObjectType = placedObjectType;
+            placedObject.origin = origin;
+            placedObject.dir = dir;
+            placedObject.actionsControllerList = new List<GameActionsController>();
+            placedObject.actionsControllerList.Add(placedObjectType.onCreateController.Duplicate());
+            placedObject.actionsControllerList.Add(placedObjectType.onUpdateController.Duplicate());
+            placedObject.actionsControllerList.Add(placedObjectType.onRemoveController.Duplicate());
+            for (int i = 0; i < placedObjectType.objectActions.Count; i++)
+            {
+                placedObject.actionsControllerList.Add( placedObjectType.objectActions[i].actions.Duplicate());
+            }
+            placedObject.context = placedObjectType.context.Duplicate();
+            foreach (var ctrl in placedObject.actionsControllerList) ctrl.context = placedObject.context;
+            // Set Game Action Variables
+            var targetVar = placedObject.context.GetVariable("Building GameObject");
+            if(targetVar == null)
+            {
+                targetVar = new GameActionContextGameObject();
+                targetVar.name = "Building GameObject";
+                targetVar.canEdit = false;
+                placedObject.context.variables.Add(targetVar);
+            }
+            ((GameActionContextGameObject)targetVar).value = placedObject.gameObject;
+            placedObject.actionsControllerList[0].Start(); // Run the OnAdd actions
+            if(playCreatedSound)
+            {
+                SoundController.EmmitSound(placedObjectType.createdSound, placedObject.transform.position);
+            }
+            return placedObject;
+        }
+
+        private void Start()
+        {
+            TooltipTrigger tooltipTrigger = gameObject.GetComponent<TooltipTrigger>();
+            if(tooltipTrigger == null) tooltipTrigger = gameObject.GetComponentInChildren<TooltipTrigger>();
+            if(tooltipTrigger != null)
+            {
+                tooltipTrigger.header = placedObjectType.title;
+                tooltipTrigger.content = placedObjectType.description;
+            }
+            var selectableTrigger = gameObject.GetComponentInChildren<SelectableObjectTrigger>();
+            if(selectableTrigger != null) selectableTrigger.selectableObject = this;
+            else Debug.LogError("No SelectableObjectTrigger found on placed object hierarchy", gameObject);
+        }
+
+        private void Update()
+        {
+            actionsControllerList[1].Start();
+        }
+
+        private void OnDestroy()
+        {
+            
+        }
+
+        public List<Vector2Int> GetGridPositionList()
+        {
+            return placedObjectType.GetGridPositionList(origin, dir);
+        }
+
+        public List<SelectableObjectAction> actions => placedObjectType.objectActions;
+
+        public void ExecuteAction(SelectableObjectAction action)
+        {
+            for (int i = 0; i < placedObjectType.objectActions.Count; i++)
+            {
+                if(action == placedObjectType.objectActions[i])
+                {
+                    if(actionsControllerList[3 + i] != null) 
+                    {
+                        if(actionsControllerList[3 + i].started) actionsControllerList[3 + i].Restart();
+                        actionsControllerList[3 + i].Start();
+                    }
+                    else Debug.LogError($"Could not find GameActionController for \"{action.title} in {gameObject.name} PlacedObject", this.gameObject);
+                }
+            }
+        }
+
+        public void DestroySelf()
+        {
+            actionsControllerList[2].Start();
+            Timer.Execute(destroyDelay, () => {
+                Destroy(gameObject);
+            }); 
+        }
+
+        public bool HasConnection(string connector, int x, int y)
+        {
+            for (int i = 0; i < placedObjectType.connectors.Count; i++)
+            {
+                Vector2Int pos = origin;
+                pos = pos + placedObjectType.GetConnectorPositionOffset(i, dir);
+                //Debug.Log($"{placedObjectType.connectors[i].position} - {placedObjectType.GetConnectorPositionOffset(i, dir)}");
+                Vector2Int targetPos = pos + placedObjectType.GetConnectorTargetPositionOffset(i, dir);
+                //Debug.Log($"Checking connector  {pos} -->{targetPos}  | {placedObjectType.connectors[i].dir.ToString()}");
+                if(targetPos.x == x && targetPos.y == y && connector == placedObjectType.connectors[i].title) return true;
+                
+            }
+            return false;
+        }
+
+        
+    }
+}
+

+ 11 - 0
Runtime/PlacedObject.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 6ff6ea3208362424faa87107b71fbfbe
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 299 - 0
Runtime/PlacedObjectType.cs

@@ -0,0 +1,299 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using KairoEngine.Core;
+using KairoEngine.Core.GameActions;
+using KairoEngine.Stockpiles;
+using Sirenix.OdinInspector;
+using KairoEngine.GameTools.Selectables;
+using KairoEngine.SFX;
+
+namespace KairoEngine.Grids
+{
+    [CreateAssetMenu(fileName = "PlaceObjectType", menuName = "KairoEngine/BuildingSystem/PlaceObjectType")]
+    [HideMonoScript]
+    public class PlacedObjectType : ScriptableObject
+    {
+        public static Dir GetNextDir(Dir dir) 
+        {
+            switch (dir)
+            {
+                default:
+                case Dir.Down:  return Dir.Left;
+                case Dir.Left:  return Dir.Up;
+                case Dir.Up:    return Dir.Right;
+                case Dir.Right: return Dir.Down;
+            }
+        }
+
+        public enum Dir 
+        {
+            Down,
+            Up,
+            Left,
+            Right
+        }
+        [BoxGroup("Properties", showLabel: false)] public string title;
+        [BoxGroup("Properties")] public string category;
+        [BoxGroup("Properties")] public Transform prefab;
+        [BoxGroup("Properties")] public Transform visual;
+        [BoxGroup("Properties")] public Sprite image;
+        [BoxGroup("Properties")] public Sprite icon;
+        [BoxGroup("Properties")] public SFXClip createdSound;
+        [BoxGroup("Properties")] public SFXClip removedSound;
+        [BoxGroup("Properties")] public bool isDestructable = true;
+        [BoxGroup("Properties"), HorizontalGroup("Properties/size"), LabelText("Width/Height")] public int width;
+        [BoxGroup("Properties"), HorizontalGroup("Properties/size", 0.3f), HideLabel] public int height;
+        [BoxGroup("Properties"), TextArea(2, 8), HideLabel, PropertySpace(4, 4)] public string description;
+
+        [PropertySpace(1, 1)] public List<KairoEngine.Stockpiles.Stockpile> cost = new List<KairoEngine.Stockpiles.Stockpile>();
+
+        [PropertySpace(1, 1)] public List<PlaceableObjectConnector> connectors = new List<PlaceableObjectConnector>();
+        [PropertySpace(1, 1)] public List<BuildingEffectArea> effectAreas = new List<BuildingEffectArea>();
+
+        [FoldoutGroup("Rules"), PropertySpace(2, 2)]
+        [InfoBox("Rules for positioning the building in the game world", InfoMessageType.Info)]
+        public ConditionType rulesetCondition;
+
+        [ListDrawerSettings(HideAddButton = false, HideRemoveButton = false, DraggableItems = false, Expanded = true, ShowPaging = false, ShowItemCount = true)]
+        [PropertySpace(2, 2)]
+        [FoldoutGroup("Rules")]
+        public List<PlaceableObjectRuleGroup> ruleset = new List<PlaceableObjectRuleGroup>();
+
+        [FoldoutGroup("Functionality")]
+        [HideLabel, InlineProperty, OnInspectorInit("SetupContext"), PropertySpace(2, 4)]
+        public GameActionContext context = new GameActionContext();
+
+        [FoldoutGroup("Functionality")]
+        [TabGroup("Functionality/Triggers", "On Action"), PropertySpace(1, 2)]
+        [InfoBox("Actions that can be triggered by the player or game events", InfoMessageType.Info)]
+        [LabelText("Actions")]
+        public List<SelectableObjectAction> objectActions;
+
+        [FoldoutGroup("Functionality")]
+        [TabGroup("Functionality/Triggers", "On Create"), InlineProperty, HideLabel, PropertySpace(1, 2)]
+        [InfoBox("Execute actions when the building is placed", InfoMessageType.Info)]
+        public GameActionsController onCreateController;
+
+        [FoldoutGroup("Functionality")]
+        [TabGroup("Functionality/Triggers", "On Update"), InlineProperty, HideLabel, PropertySpace(1, 2)]
+        [InfoBox("Executes every frame while the building is active", InfoMessageType.Info)]
+        public GameActionsController onUpdateController;
+
+        [FoldoutGroup("Functionality")]
+        [TabGroup("Functionality/Triggers", "On Remove"), InlineProperty, HideLabel, PropertySpace(1, 2)]
+        [InfoBox("Executes when the building is removed", InfoMessageType.Info)]
+        public GameActionsController onRemoveController;
+
+        public int GetRotationAngle(Dir dir) 
+        { 
+            switch (dir)
+            {
+                default:
+                case Dir.Down:  return 0;
+                case Dir.Left:  return 90;
+                case Dir.Up:    return 180;
+                case Dir.Right: return 270;
+            }
+        }
+
+        public Vector2Int GetRotationOffset(Dir dir) 
+        { 
+            switch (dir)
+            {
+                default:
+                case Dir.Down:  return new Vector2Int(0, 0);
+                case Dir.Left:  return new Vector2Int(0, width);
+                case Dir.Up:    return new Vector2Int(width, height);
+                case Dir.Right: return new Vector2Int(height, 0);
+            }
+        }
+
+        public Vector2Int GetRotationOffset(Dir dir, int width, int height) 
+        { 
+            switch (dir)
+            {
+                default:
+                case Dir.Down:  return new Vector2Int(0, 0);
+                case Dir.Left:  return new Vector2Int(0, width);
+                case Dir.Up:    return new Vector2Int(width, height);
+                case Dir.Right: return new Vector2Int(height, 0);
+            }
+        }
+
+        public List<Vector2Int> GetGridPositionList(Vector2Int offset, Dir dir) 
+        { 
+            List<Vector2Int> gridPositionList = new List<Vector2Int>();
+            switch (dir)
+            {
+                default:
+                case Dir.Down:
+                case Dir.Up:
+                    for(int x = 0; x < width; x++)
+                    {
+                        for (int y = 0; y < height; y++)
+                        {
+                            gridPositionList.Add(offset + new Vector2Int(x, y));
+                        }
+                    }
+                    break;
+                case Dir.Left:
+                case Dir.Right:
+                    for (int x = 0; x < height; x++)
+                    {
+                        for (int y = 0; y < width; y++)
+                        {
+                            gridPositionList.Add(offset + new Vector2Int(x, y));
+                        }
+                    }
+                    break;
+            }
+            return gridPositionList;
+        }
+
+        public bool CanPlaceObject(Grid<GridObject> grid, Vector3 worldPos, PlacedObjectType.Dir dir)
+        {
+            grid.GetGridPosition(worldPos, out int x, out int y);
+            return CanPlaceObject(grid, x, y, dir);
+        }
+
+        public bool CanPlaceObject(Grid<GridObject> grid,  int x, int y, PlacedObjectType.Dir dir)
+        {
+            return EvaluateRuleset(grid, x, y, dir);
+        }
+
+        public bool EvaluateRuleset(Grid<GridObject> grid,  int x, int y, PlacedObjectType.Dir dir)
+        {
+            List<bool> results = new List<bool>();
+            for (int i = 0; i < ruleset.Count; i++)
+            {
+                results.Add(ruleset[i].Evaluate(grid, this, x, y, dir));
+            }
+            if(rulesetCondition == ConditionType.AND)
+            {
+                foreach (var value in results)
+                {
+                    if(value == false) return false;
+                }
+                return true;
+            }
+            else
+            {
+                foreach (var value in results)
+                {
+                    if(value == true) return true;
+                }
+                return false;
+            }
+        }
+
+        public List<PlaceableObjectRule> GetRulesetError(Grid<GridObject> grid,  int x, int y, PlacedObjectType.Dir dir)
+        {
+            List<PlaceableObjectRule> brokenRules = new List<PlaceableObjectRule>();
+            for (int i = 0; i < ruleset.Count; i++)
+            {
+                for (int a = 0; a < ruleset[i].rules.Count; a++)
+                {
+                    bool result = ruleset[i].rules[a].Evaluate(grid, this, x, y, dir);
+                    if(result == false) brokenRules.Add(ruleset[i].rules[a]);
+                }
+            }
+            return brokenRules;
+        }
+
+        public Vector2Int GetConnectorPositionOffset(int index, PlacedObjectType.Dir dir)
+        {
+            PlaceableObjectConnector connector = connectors[index];
+            switch (dir)
+            {
+                default:
+                case Dir.Down:  return connector.position;
+                case Dir.Left:  return new Vector2Int(connector.position.y, (connector.position.x * -1) + width - 1);
+                case Dir.Up:    return new Vector2Int((connector.position.x * -1) + width - 1, (connector.position.y * -1) + height - 1);
+                case Dir.Right: return new Vector2Int((connector.position.y * -1) + height - 1, connector.position.x);
+            }
+        }
+
+        public Vector2Int GetConnectorTargetPositionOffset(int index, PlacedObjectType.Dir objectDir)
+        {
+            var targetDir = connectors[index].dir;
+            return GetPositionOffsetFromDirection(objectDir, targetDir);
+        }
+
+        public Vector2Int GetPositionOffsetFromDirection(PlacedObjectType.Dir objectDir, PlacedObjectType.Dir targetDir)
+        {
+            switch (objectDir)
+            {
+                default:
+                case Dir.Down:  
+                    switch (targetDir)
+                    {
+                        default:
+                        case Dir.Down:  return new Vector2Int(0, -1);
+                        case Dir.Left:  return new Vector2Int(-1, 0);
+                        case Dir.Up:    return new Vector2Int(0, 1);
+                        case Dir.Right: return new Vector2Int(1, 0);
+                    }
+                case Dir.Left:  
+                    switch (targetDir)
+                    {
+                        default:
+                        case Dir.Down:  return new Vector2Int(-1, 0);
+                        case Dir.Left:  return new Vector2Int(0, 1);
+                        case Dir.Up:    return new Vector2Int(1, 0);
+                        case Dir.Right: return new Vector2Int(0, -1);
+                    }
+                case Dir.Up:    
+                    switch (targetDir)
+                    {
+                        default:
+                        case Dir.Down:  return new Vector2Int(0, 1);
+                        case Dir.Left:  return new Vector2Int(1, 0);
+                        case Dir.Up:    return new Vector2Int(0, -1);
+                        case Dir.Right: return new Vector2Int(-1, 0);
+                    }
+                case Dir.Right: 
+                    switch (targetDir)
+                    {
+                        default:
+                        case Dir.Down:  return new Vector2Int(1, 0);
+                        case Dir.Left:  return new Vector2Int(0, -1);
+                        case Dir.Up:    return new Vector2Int(-1, 0);
+                        case Dir.Right: return new Vector2Int(0, 1);
+                    }
+            }
+        }
+
+        private void SetupContext()
+        {
+            if(context == null) return;
+            onCreateController.context = context;
+            onUpdateController.context = context;
+            onRemoveController.context = context;
+            for (int i = 0; i < objectActions.Count; i++)
+            {
+                objectActions[i].actions.context = context;
+            }
+            if(!context.HasVariable("Building GameObject"))
+            {
+                var variable = new GameActionContextGameObject();
+                variable.name = "Building GameObject";
+                variable.value = null;
+                variable.canEdit = false;
+                context.variables.Add(variable);
+            } 
+        }
+
+        public Vector2Int OffsetValue(Vector2Int offset, Dir dir)
+        {
+            switch (dir)
+            {
+                default:
+                case Dir.Down:  return new Vector2Int(offset.x, offset.y);
+                case Dir.Left:  return new Vector2Int(offset.y, offset.x * -1);
+                case Dir.Up:    return new Vector2Int(offset.x * -1, offset.y * -1);
+                case Dir.Right: return new Vector2Int(offset.y * -1, offset.x);
+            }
+        }
+    }
+}

+ 11 - 0
Runtime/PlacedObjectType.cs.meta

@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b3b979d268eb75d40bd23b89c3656386
+MonoImporter:
+  externalObjects: {}
+  serializedVersion: 2
+  defaultReferences: []
+  executionOrder: 0
+  icon: {instanceID: 0}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 8 - 0
Tests.meta

@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5fd80378e08f11546a6a9f2e70746d9a
+folderAsset: yes
+DefaultImporter:
+  externalObjects: {}
+  userData: 
+  assetBundleName: 
+  assetBundleVariant: 

+ 19 - 0
package.json

@@ -0,0 +1,19 @@
+{
+    "name": "at.kairoscope.kairoengine.grids",
+    "displayName": "KairoEngine Grids",
+    "version": "0.1.2",
+    "unity": "2020.3",
+    "description": "Kairoengine grids library.",
+    "dependencies": {
+        "at.kairoscope.thirdparty.sirenix":"1.0.0",
+        "at.kairoscope.kairoengine.core":"0.1.6",
+        "at.kairoscope.kairoengine.ui":"0.1.2",
+        "at.kairoscope.kairoengine.stockpiles":"0.1.1",
+        "at.kairoscope.kairoengine.utilities":"0.1.2"
+    },
+    "repository": {
+      "type": "git",
+      "url": "https://git.kairoscope.net/kairoengine/grids.git"
+    },
+    "author": "Kairoscope"
+  }