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 [Export] public bool Enabled { get; set; } = true; [Export] public StringName ToggleBuildAction { get; set; } = "toggle_build"; private static readonly List BuildableTiles = ["wall", "miner", "turret"]; private readonly Dictionary _buildTasks = new(); private AudioStreamPlayer _completionSound; private AudioStreamPlayer _buildingSound; private AudioStreamPlayer _canceledSound; private AudioStreamPlayer _cannotDeploySound; private AudioStreamPlayer _notReadySound; private AudioStreamPlayer _insufficientFundsSound; private Node2D _currentGhost; // Keep track of the current ghost building public override void _Ready() { base._Ready(); // Setup completion sound _completionSound = CreateAudioPlayer("res://Sounds/Events/ConstructionComplete.wav"); _buildingSound = CreateAudioPlayer("res://Sounds/Events/Building.wav"); _canceledSound = CreateAudioPlayer("res://Sounds/Events/Canceled.wav"); _cannotDeploySound = CreateAudioPlayer("res://Sounds/Events/CannotDeployHere.wav"); _notReadySound = CreateAudioPlayer("res://Sounds/Events/NotReady.wav"); _insufficientFundsSound = CreateAudioPlayer("res://Sounds/Events/InsufficientFunds.wav"); } private AudioStreamPlayer CreateAudioPlayer(string path) { var player = new AudioStreamPlayer(); AddChild(player); var sound = GD.Load(path); if (sound != null) { player.Stream = sound; } return player; } 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) { if (!Enabled) return; // 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.Grid = Grid; _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")) { var building = Registry.GetBuilding(_currentBuildingId); if (building == null) return; if (!CanStartNewBuild()) { _notReadySound.Play(); return; } // First check if area is free if (!IsAreaFree(_hoveredCell, building.Size, _currentRotation, building.Layer)) { // Check if the area is occupied by under-construction tiles var occupiedCells = GridUtils.GetOccupiedCells(_hoveredCell, building.Size, _currentRotation); var isUnderConstruction = occupiedCells.Any(cell => Grid.GetTileAtCell(cell, building.Layer) is BaseTile { IsConstructing: true }); if (!isUnderConstruction) _cannotDeploySound.Play(); return; } // Consume resources first if (!ConsumeBuildingResources(_currentBuildingId)) { _insufficientFundsSound.Play(); return; } // Create the building instance var scene = building.Scene; var buildingInstance = (BaseTile)scene.Instantiate(); buildingInstance.Grid = Grid; buildingInstance.RotationDegrees = _currentRotation; buildingInstance.ZIndex = (int)building.Layer; buildingInstance.Position = _ghostBuilding.Position; AddChild(buildingInstance); // If we get here, area is free, so we can safely occupy it Grid.OccupyArea(_hoveredCell, buildingInstance, building.Size, _currentRotation, building.Layer); if (building.BuildTime > 0f) { var wasQueueEmpty = _buildTasks.Count == 0; var buildTask = new BuildTask(OnBuildCompleted); _buildTasks[buildingInstance] = buildTask; // Play building sound only when adding to an empty queue if (wasQueueEmpty) _buildingSound.Play(); 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.GetTileAtCell(_hoveredCell); if (building == null) return; // Find all cells occupied by this building var buildingInfo = Grid.GetBuildingInfoAtCell(_hoveredCell, GridLayer.Building); if (buildingInfo == null) return; // Check if this building is in the build tasks (under construction) if (_buildTasks.TryGetValue(building, out var buildTask)) { var buildingTile = building as BaseTile; // Cancel the build task buildTask.Complete(true); // Mark as cancelled _buildTasks.Remove(building); _canceledSound.Play(); if (buildingTile == null) return; // Refund resources for canceled build var buildingData = Registry.GetBuilding(buildingTile.TileId); if (buildingData != null) RefundBuildingResources(buildingTile.TileId); } // Clean up the building and grid 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 _Input(InputEvent @event) { if (@event.IsActionPressed(ToggleBuildAction)) { Enabled = !Enabled; // Hide ghost building when disabling if (!Enabled && _ghostBuilding != null && _ghostBuilding.IsInsideTree()) { _ghostBuilding.QueueFree(); _ghostBuilding = null; } } } 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.GetTileAtCell(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.GetTileAtCell(checkCell, layer) == building) { // Found the top-left corner, now find the size var size = Vector2I.One; // Search right while (grid.GetTileAtCell(new Vector2I(checkCell.X + size.X, checkCell.Y), layer) == building) size.X++; // Search down while (grid.GetTileAtCell(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; } }