diff --git a/Data/Buildings.json b/Data/Buildings.json index 8ed95a3..139ceb4 100644 --- a/Data/Buildings.json +++ b/Data/Buildings.json @@ -5,7 +5,9 @@ "stone": 5 }, "durability": 100, - "buildTime": 1.5 + "buildTime": 1.5, + "allowedRotations": [0], + "layer": 1 }, "miner": { "scene": "res://Scenes/Tiles/MinerTile.tscn", @@ -14,6 +16,8 @@ "stone": 3 }, "durability": 200, - "buildTime": 3.0 + "buildTime": 3.0, + "allowedRotations": [0], + "layer": 1 } } diff --git a/Scripts/System/BuildingRegistry.cs b/Scripts/System/BuildingRegistry.cs index 1e5c429..e54c980 100644 --- a/Scripts/System/BuildingRegistry.cs +++ b/Scripts/System/BuildingRegistry.cs @@ -1,17 +1,46 @@ using System.Collections.Generic; +using System.Linq; using Godot; namespace AceFieldNewHorizon.Scripts.System; +public enum RotationDirection +{ + Up, // 0 degrees + Right, // 90 degrees + Down, // 180 degrees + Left // 270 degrees +} + +public record BuildingData( + PackedScene Scene, + Dictionary Cost, + int Durability, + GridLayer Layer, + float BuildTime, + Vector2I Size = default, + RotationDirection[] AllowedRotations = null +) +{ + public Vector2I Size { get; } = Size == default ? Vector2I.One : Size; + public RotationDirection[] AllowedRotations { get; } = + AllowedRotations ?? [RotationDirection.Up, RotationDirection.Right, RotationDirection.Down, RotationDirection.Left + ]; + + public bool IsRotationAllowed(float degrees) + { + var direction = (RotationDirection)((Mathf.RoundToInt(degrees / 90f) % 4 + 4) % 4); + return AllowedRotations.Contains(direction); + } + + public Vector2I GetRotatedSize(float degrees) + { + return (Mathf.RoundToInt(degrees / 90f) % 2) == 0 ? Size : new Vector2I(Size.Y, Size.X); + } +} + public partial class BuildingRegistry : Node { - public record BuildingData( - PackedScene Scene, - Dictionary Cost, - int Durability, - float BuildTime - ); - private Dictionary _registry = new(); [Export] public string JsonPath { get; set; } = "res://Data/Buildings.json"; @@ -45,7 +74,7 @@ public partial class BuildingRegistry : Node foreach (string key in dict.Keys) { - // Each entry is a Dictionary with keys: "scene", "cost", "durability", "buildTime" + // Each entry is a Dictionary with keys: "scene", "cost", "durability", "buildTime", "size", "allowedRotations" var buildingDict = dict[key].AsGodotDictionary(); // Parse scene @@ -115,7 +144,36 @@ public partial class BuildingRegistry : Node } } - var buildingData = new BuildingData(scene, cost, durability, buildTime); + // Parse size + var size = Vector2I.One; + if (buildingDict.TryGetValue("size", out var sObj)) + { + var sizeArray = sObj.AsGodotArray(); + if (sizeArray.Count == 2) + { + size = new Vector2I((int)sizeArray[0].AsInt64(), (int)sizeArray[1].AsInt64()); + } + } + + // Parse allowedRotations + RotationDirection[] allowedRotations = null; + if (buildingDict.TryGetValue("allowedRotations", out var arObj)) + { + var arArray = arObj.AsGodotArray(); + allowedRotations = new RotationDirection[arArray.Count]; + for (var i = 0; i < arArray.Count; i++) + { + allowedRotations[i] = (RotationDirection)arArray[i].AsInt32(); + } + } + + var layer = GridLayer.Building; + if (buildingDict.TryGetValue("layer", out var lObj)) + { + layer = (GridLayer)lObj.AsInt32(); + } + + var buildingData = new BuildingData(scene, cost, durability, layer, buildTime, size, allowedRotations); _registry[key] = buildingData; GD.Print($"[BuildingRegistry] Loaded building '{key}' from {scenePath}"); } diff --git a/Scripts/System/GridManager.cs b/Scripts/System/GridManager.cs index 4897280..3772765 100644 --- a/Scripts/System/GridManager.cs +++ b/Scripts/System/GridManager.cs @@ -7,43 +7,60 @@ namespace AceFieldNewHorizon.Scripts.System; public enum GridLayer { - Ground, // Base layer (e.g., terrain, floors) - Building, // Main building layer + Ground, // Base layer (e.g., terrain, floors) + Building, // Main building layer Decoration // Additional layer for decorations, effects, etc. } public partial class GridManager : Node { - private Dictionary> _layers = new(); + private Dictionary> _layers = + new(); public GridManager() { // Initialize all layers foreach (GridLayer layer in Enum.GetValues(typeof(GridLayer))) { - _layers[layer] = new Dictionary(); + _layers[layer] = new Dictionary(); } } - public bool IsCellFree(Vector2I cell, GridLayer layer = GridLayer.Building) + public bool IsAreaFree(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building) { - return !_layers[layer].ContainsKey(cell); + var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation); + foreach (var cell in occupiedCells) + { + if (_layers[layer].ContainsKey(cell)) + return false; + } + + return true; } - public void OccupyCell(Vector2I cell, Node2D building, GridLayer layer = GridLayer.Building) + public void OccupyArea(Vector2I topLeft, Node2D building, Vector2I size, float rotation, + GridLayer layer = GridLayer.Building) { - _layers[layer][cell] = building; + var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation); + foreach (var cell in occupiedCells) + { + _layers[layer][cell] = (building, size, rotation); + } } - public void FreeCell(Vector2I cell, GridLayer layer = GridLayer.Building) + public void FreeArea(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer = GridLayer.Building) { - if (_layers[layer].ContainsKey(cell)) - _layers[layer].Remove(cell); + var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation); + foreach (var cell in occupiedCells) + { + if (_layers[layer].ContainsKey(cell)) + _layers[layer].Remove(cell); + } } public Node2D? GetBuildingAtCell(Vector2I cell, GridLayer layer = GridLayer.Building) { - return _layers[layer].GetValueOrDefault(cell); + return _layers[layer].TryGetValue(cell, out var data) ? data.Building : null; } public bool IsAnyCellOccupied(Vector2I cell, params GridLayer[] layers) @@ -55,6 +72,7 @@ public partial class GridManager : Node { if (layer.ContainsKey(cell)) return true; } + return false; } @@ -62,6 +80,34 @@ public partial class GridManager : Node { if (_layers[layer].ContainsKey(cell)) return true; } + + return false; + } + + public bool IsAreaOccupied(Vector2I topLeft, Vector2I size, float rotation, params GridLayer[] layers) + { + var occupiedCells = GridUtils.GetOccupiedCells(topLeft, size, rotation); + + if (layers.Length == 0) + { + // Check all layers if none specified + foreach (var cell in occupiedCells) + { + foreach (var layer in _layers.Values) + if (layer.ContainsKey(cell)) + return true; + } + + return false; + } + + foreach (var cell in occupiedCells) + { + foreach (var layer in layers) + if (_layers[layer].ContainsKey(cell)) + return true; + } + return false; } } @@ -85,4 +131,26 @@ public static class GridUtils cell.Y * TileSize ); } + + public static Vector2 GetCenterOffset(Vector2I size, float rotation) + { + var rotatedSize = (Mathf.RoundToInt(rotation / 90f) % 2) == 0 ? size : new Vector2I(size.Y, size.X); + return new Vector2(rotatedSize.X * TileSize / 2f, rotatedSize.Y * TileSize / 2f); + } + + public static List GetOccupiedCells(Vector2I topLeft, Vector2I size, float rotation) + { + var occupiedCells = new List(); + var rotatedSize = (Mathf.RoundToInt(rotation / 90f) % 2) == 0 ? size : new Vector2I(size.Y, size.X); + + for (int x = 0; x < rotatedSize.X; x++) + { + for (int y = 0; y < rotatedSize.Y; y++) + { + occupiedCells.Add(new Vector2I(topLeft.X + x, topLeft.Y + y)); + } + } + + return occupiedCells; + } } \ No newline at end of file diff --git a/Scripts/System/PlacementManager.cs b/Scripts/System/PlacementManager.cs index 608e1fb..80572a4 100644 --- a/Scripts/System/PlacementManager.cs +++ b/Scripts/System/PlacementManager.cs @@ -1,6 +1,7 @@ using System; using AceFieldNewHorizon.Scripts.Tiles; using Godot; +using System.Linq; namespace AceFieldNewHorizon.Scripts.System; @@ -10,26 +11,26 @@ public partial class PlacementManager : Node2D [Export] public BuildingRegistry Registry { get; set; } private string _currentBuildingId = "wall"; - private GridLayer _currentLayer = GridLayer.Building; // Default layer private Vector2I _hoveredCell; private BaseTile _ghostBuilding; - private float _currentRotation = 0f; + private float _currentRotation; + private Vector2I _currentBuildingSize = Vector2I.One; - public void SetCurrentBuilding(string buildingId, GridLayer layer = GridLayer.Building) + public void SetCurrentBuilding(string buildingId) { _currentBuildingId = buildingId; - _currentLayer = layer; + var buildingData = Registry.GetBuilding(buildingId); + if (buildingData != null) + { + _currentBuildingSize = buildingData.Size; + // Reset rotation to nearest allowed rotation + if (!buildingData.IsRotationAllowed(_currentRotation)) + { + _currentRotation = (int)buildingData.AllowedRotations[0] * 90f; + } + } // Replace ghost immediately - if (_ghostBuilding == null) return; - _ghostBuilding.QueueFree(); - _ghostBuilding = null; - } - - public void SetCurrentLayer(GridLayer layer) - { - _currentLayer = layer; - // Update ghost building if needed if (_ghostBuilding != null) { _ghostBuilding.QueueFree(); @@ -40,18 +41,54 @@ public partial class PlacementManager : Node2D private void RotateGhost(bool reverse = false) { if (_ghostBuilding == null) return; + + var buildingData = Registry.GetBuilding(_currentBuildingId); + if (buildingData == null) return; + + // Calculate next rotation + var currentDirection = (RotationDirection)((Mathf.RoundToInt(_currentRotation / 90f) % 4 + 4) % 4); + var currentIndex = Array.IndexOf(buildingData.AllowedRotations.ToArray(), currentDirection); + if (reverse) - _currentRotation = (_currentRotation - 90f) % 360f; + currentIndex = (currentIndex - 1 + buildingData.AllowedRotations.Length) % + buildingData.AllowedRotations.Length; else - _currentRotation = (_currentRotation + 90f) % 360f; + currentIndex = (currentIndex + 1) % buildingData.AllowedRotations.Length; + + _currentRotation = (int)buildingData.AllowedRotations[currentIndex] * 90f; _ghostBuilding.RotationDegrees = _currentRotation; + + // Update ghost position to keep the same cell under cursor + UpdateGhostPosition(); + } + + private void UpdateGhostPosition() + { + if (_ghostBuilding == null) return; + + var buildingData = Registry.GetBuilding(_currentBuildingId); + if (buildingData == null) return; + + var rotatedSize = buildingData.GetRotatedSize(_currentRotation); + var offset = GridUtils.GetCenterOffset(rotatedSize, _currentRotation); + _ghostBuilding.Position = GridUtils.GridToWorld(_hoveredCell) + offset; + + // Update occupied cells + GridUtils.GetOccupiedCells(_hoveredCell, rotatedSize, _currentRotation); } public override void _Process(double delta) { // Snap mouse to grid var mousePos = GetGlobalMousePosition(); - _hoveredCell = GridUtils.WorldToGrid(mousePos); + var newHoveredCell = GridUtils.WorldToGrid(mousePos); + + // Only update if cell changed + if (newHoveredCell != _hoveredCell) + { + _hoveredCell = newHoveredCell; + UpdateGhostPosition(); + } if (Input.IsActionJustPressed("rotate_tile_reverse")) RotateGhost(reverse: true); @@ -60,82 +97,115 @@ public partial class PlacementManager : Node2D if (_ghostBuilding == null) { - var scene = Registry.GetBuilding(_currentBuildingId)?.Scene; - if (scene == null) return; + var building = Registry.GetBuilding(_currentBuildingId); + if (building == null) return; + var scene = building.Scene; _ghostBuilding = (BaseTile)scene.Instantiate(); _ghostBuilding.SetGhostMode(true); _ghostBuilding.RotationDegrees = _currentRotation; _ghostBuilding.ZAsRelative = false; - _ghostBuilding.ZIndex = (int)_currentLayer; // Use layer as Z-index + _ghostBuilding.ZIndex = (int)building.Layer; + UpdateGhostPosition(); AddChild(_ghostBuilding); } - var placementPos = GridUtils.GridToWorld(_hoveredCell); - var spaceState = GetWorld2D().DirectSpaceState; - var centerPos = placementPos + new Vector2(GridUtils.TileSize / 2f, GridUtils.TileSize / 2f); - - var query = new PhysicsPointQueryParameters2D - { - Position = centerPos, - CollideWithBodies = true, - CollideWithAreas = true, - CollisionMask = uint.MaxValue - }; - - var collision = spaceState.IntersectPoint(query, 8); - - // Check if the current cell is free in the current layer - var canPlace = Grid.IsCellFree(_hoveredCell, _currentLayer) && - !Grid.IsAnyCellOccupied(_hoveredCell, GetBlockingLayers()); - - _ghostBuilding.Position = placementPos; + var canPlace = CanPlaceBuilding(); _ghostBuilding.SetGhostMode(canPlace); // Left click to place if (Input.IsActionPressed("build_tile") && canPlace) { - var scene = Registry.GetBuilding(_currentBuildingId)?.Scene; - if (scene == null) return; + var building = Registry.GetBuilding(_currentBuildingId); + if (building == null) return; - _ghostBuilding.FinalizePlacement(); - _ghostBuilding.RotationDegrees = _currentRotation; - _ghostBuilding.ZIndex = (int)_currentLayer; + var scene = building.Scene; + var buildingInstance = (BaseTile)scene.Instantiate(); + buildingInstance.RotationDegrees = _currentRotation; + buildingInstance.ZIndex = (int)building.Layer; + buildingInstance.Position = _ghostBuilding.Position; + AddChild(buildingInstance); - var buildingData = Registry.GetBuilding(_currentBuildingId); - Grid.OccupyCell(_hoveredCell, _ghostBuilding, _currentLayer); + Grid.OccupyArea(_hoveredCell, buildingInstance, building.Size, _currentRotation, building.Layer); - if (buildingData is { BuildTime: > 0f }) - _ghostBuilding.StartConstruction(buildingData.BuildTime); - - // Create new ghost for next placement - _ghostBuilding = (BaseTile)scene.Instantiate(); - _ghostBuilding.SetGhostMode(true); - _ghostBuilding.RotationDegrees = _currentRotation; - _ghostBuilding.ZAsRelative = false; - _ghostBuilding.ZIndex = (int)_currentLayer; - AddChild(_ghostBuilding); + if (building.BuildTime > 0f) + buildingInstance.StartConstruction(building.BuildTime); } - if (Input.IsActionPressed("destroy_tile") && !Grid.IsCellFree(_hoveredCell, _currentLayer)) + if (Input.IsActionPressed("destroy_tile") && + !Grid.IsAreaFree(_hoveredCell, Vector2I.One, 0f)) { // Right click to destroy from current layer - var building = Grid.GetBuildingAtCell(_hoveredCell, _currentLayer); + var building = Grid.GetBuildingAtCell(_hoveredCell); if (building == null) return; + // Find all cells occupied by this building + var buildingInfo = Grid.GetBuildingInfoAtCell(_hoveredCell, GridLayer.Building); + if (buildingInfo == null) return; building.QueueFree(); - Grid.FreeCell(_hoveredCell, _currentLayer); + Grid.FreeArea(buildingInfo.Value.Position, buildingInfo.Value.Size, buildingInfo.Value.Rotation); } } - // Define which layers should block placement - private GridLayer[] GetBlockingLayers() + private bool CanPlaceBuilding() { - return _currentLayer switch + var buildingData = Registry.GetBuilding(_currentBuildingId); + if (buildingData == null) return false; + + // Check if rotation is allowed + if (!buildingData.IsRotationAllowed(_currentRotation)) + return false; + + // Check if area is free + var rotatedSize = buildingData.GetRotatedSize(_currentRotation); + return !Grid.IsAreaOccupied(_hoveredCell, rotatedSize, _currentRotation, GetBlockingLayers(buildingData.Layer)); + } + + private GridLayer[] GetBlockingLayers(GridLayer layer) + { + return layer switch { - GridLayer.Ground => new[] { GridLayer.Ground }, - GridLayer.Building => new[] { GridLayer.Ground, GridLayer.Building }, - GridLayer.Decoration => new[] { GridLayer.Ground, GridLayer.Building, GridLayer.Decoration }, + GridLayer.Ground => [GridLayer.Ground], + GridLayer.Building => [GridLayer.Ground, GridLayer.Building], + GridLayer.Decoration => [GridLayer.Ground, GridLayer.Building, GridLayer.Decoration], _ => [] }; } +} + +public static class GridManagerExtensions +{ + public static (Vector2I Position, Vector2I Size, float Rotation)? GetBuildingInfoAtCell(this GridManager grid, + Vector2I cell, GridLayer layer) + { + if (grid.GetBuildingAtCell(cell, layer) is { } building) + { + // Find the top-left position of the building + for (int x = 0; x < 100; x++) // Arbitrary max size + { + for (int y = 0; y < 100; y++) + { + var checkCell = new Vector2I(cell.X - x, cell.Y - y); + if (grid.GetBuildingAtCell(checkCell, layer) == building) + { + // Found the top-left corner, now find the size + var size = Vector2I.One; + // Search right + while (grid.GetBuildingAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) == + building) + size.X++; + // Search down + while (grid.GetBuildingAtCell(new Vector2I(checkCell.X, checkCell.Y + size.Y), layer) == + building) + size.Y++; + + // Get rotation from the first cell + var rotation = 0f; // You'll need to store rotation in GridManager to make this work + return (checkCell, size, rotation); + } + } + } + } + + return null; + } } \ No newline at end of file