using System; using System.Collections.Generic; using AceFieldNewHorizon.Scripts.Tiles; using Godot; using System.Linq; namespace AceFieldNewHorizon.Scripts.System; public partial class PlacementManager : Node2D { [Export] public GridManager Grid { get; set; } [Export] public ResourceManager Inventory { get; set; } [Export] public BuildingRegistry Registry { get; set; } [Export] public int MaxConcurrentBuilds { get; set; } = 6; // Make it adjustable in editor private static readonly List BuildableTiles = ["wall", "miner"]; private readonly Dictionary _buildTasks = new(); private AudioStreamPlayer _completionSound; public override void _Ready() { base._Ready(); // Setup completion sound _completionSound = new AudioStreamPlayer(); AddChild(_completionSound); var sound = GD.Load("res://Sounds/Events/ConstructionComplete.wav"); if (sound != null) { _completionSound.Stream = sound; } } private void OnBuildCompleted() { // Remove all completed builds var completed = _buildTasks.Where(kvp => kvp.Value.IsCompleted) .Select(kvp => kvp.Key) .ToList(); foreach (var key in completed) { _buildTasks.Remove(key); } // If no builds left, play the completion sound if (_buildTasks.Count == 0) { _completionSound.Play(); } } // Call this when starting a new build private bool CanStartNewBuild() { // Remove completed builds var completed = _buildTasks.Where(kvp => kvp.Value.IsCompleted) .Select(kvp => kvp.Key) .ToList(); foreach (var key in completed) { _buildTasks.Remove(key); } return _buildTasks.Count < MaxConcurrentBuilds; } private class BuildTask(Action onCompleted) : IDisposable { public bool IsCompleted { get; private set; } public bool WasCancelled { get; private set; } public void Complete(bool wasCancelled = false) { if (IsCompleted) return; IsCompleted = true; WasCancelled = wasCancelled; onCompleted?.Invoke(); } public void Dispose() { Complete(true); // Mark as cancelled if disposed before completion } } private string _currentBuildingId = "wall"; private Vector2I _hoveredCell; private BaseTile _ghostBuilding; private float _currentRotation; private Vector2I _currentBuildingSize = Vector2I.One; public void SetCurrentBuilding(string buildingId) { _currentBuildingId = buildingId; 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) { _ghostBuilding.QueueFree(); _ghostBuilding = null; } } 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) currentIndex = (currentIndex - 1 + buildingData.AllowedRotations.Length) % buildingData.AllowedRotations.Length; else 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(); 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); else if (Input.IsActionJustPressed("rotate_tile")) RotateGhost(); if (_ghostBuilding == null) { 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)building.Layer; UpdateGhostPosition(); AddChild(_ghostBuilding); } var canPlace = CanPlaceBuilding(); _ghostBuilding.SetGhostMode(canPlace); // Left click to place if (Input.IsActionPressed("build_tile") && canPlace) { var building = Registry.GetBuilding(_currentBuildingId); if (building == null) return; if (!CanStartNewBuild()) { // Optionally show feedback to player that build queue is full return; } // Consume resources first if (!ConsumeBuildingResources(_currentBuildingId)) { // Optionally show feedback to player that they can't afford this building return; } var scene = building.Scene; var buildingInstance = (BaseTile)scene.Instantiate(); buildingInstance.RotationDegrees = _currentRotation; buildingInstance.ZIndex = (int)building.Layer; buildingInstance.Position = _ghostBuilding.Position; AddChild(buildingInstance); // Check if area is free before placing if (!IsAreaFree(_hoveredCell, building.Size, _currentRotation, building.Layer)) { RefundBuildingResources(_currentBuildingId); buildingInstance.QueueFree(); return; } // Occupy the area Grid.OccupyArea(_hoveredCell, buildingInstance, building.Size, _currentRotation, building.Layer); if (building.BuildTime > 0f) { var buildTask = new BuildTask(OnBuildCompleted); _buildTasks[buildingInstance] = buildTask; buildingInstance.StartConstruction(building.BuildTime, () => { // On construction complete if (_buildTasks.TryGetValue(buildingInstance, out var task)) { if (task.WasCancelled) { RefundBuildingResources(_currentBuildingId); Grid.FreeArea(_hoveredCell, building.Size, _currentRotation, building.Layer); buildingInstance.QueueFree(); } task.Complete(); _buildTasks.Remove(buildingInstance); } }); } } if (Input.IsActionPressed("destroy_tile") && !Grid.IsAreaFree(_hoveredCell, Vector2I.One, 0f)) { // Right click to destroy from current layer 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.FreeArea(buildingInfo.Value.Position, buildingInfo.Value.Size, buildingInfo.Value.Rotation); } if (Input.IsActionJustPressed("switch_tile")) { var currentIdx = BuildableTiles.IndexOf(_currentBuildingId); var nextIdx = (currentIdx + 1) % BuildableTiles.Count; SetCurrentBuilding(BuildableTiles[nextIdx]); } } public override void _ExitTree() { base._ExitTree(); _buildTasks.Clear(); } private bool CanAffordBuilding(string buildingId) { if (Inventory == null) return false; var buildingData = Registry.GetBuilding(buildingId); if (buildingData == null) return false; // Check if we have enough of each required resource foreach (var (resourceId, amount) in buildingData.Cost) if (!Inventory.HasResource(resourceId, amount)) return false; return true; } private bool ConsumeBuildingResources(string buildingId) { if (Inventory == null) return false; var buildingData = Registry.GetBuilding(buildingId); if (buildingData == null) return false; // First verify we can afford it if (!CanAffordBuilding(buildingId)) return false; // Then consume each resource foreach (var (resourceId, amount) in buildingData.Cost) Inventory.RemoveResource(resourceId, amount); return true; } private void RefundBuildingResources(string buildingId) { if (Inventory == null) return; var buildingData = Registry.GetBuilding(buildingId); if (buildingData == null) return; // Refund each resource foreach (var (resourceId, amount) in buildingData.Cost) { Inventory.AddResource(resourceId, amount); } } private bool CanPlaceBuilding() { var buildingData = Registry.GetBuilding(_currentBuildingId); if (buildingData == null) return false; // Check if we can afford the building if (!CanAffordBuilding(_currentBuildingId)) 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 => [GridLayer.Ground], GridLayer.Building => [GridLayer.Building], GridLayer.Decoration => [GridLayer.Decoration], _ => [] }; } private bool IsAreaFree(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer) { return !Grid.IsAreaOccupied(topLeft, size, rotation, GetBlockingLayers(layer)); } } 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; } }