diff --git a/Scripts/System/PlacementManager.cs b/Scripts/System/PlacementManager.cs index 7a1a645..1446e93 100644 --- a/Scripts/System/PlacementManager.cs +++ b/Scripts/System/PlacementManager.cs @@ -11,7 +11,7 @@ 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"; @@ -19,33 +19,50 @@ public partial class PlacementManager : Node2D private static readonly List BuildableTiles = ["wall", "miner"]; 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 = new AudioStreamPlayer(); - AddChild(_completionSound); - var sound = GD.Load("res://Sounds/Events/ConstructionComplete.wav"); + _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) { - _completionSound.Stream = sound; + 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(); + .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) { @@ -58,13 +75,13 @@ public partial class PlacementManager : Node2D { // Remove completed builds var completed = _buildTasks.Where(kvp => kvp.Value.IsCompleted) - .Select(kvp => kvp.Key) - .ToList(); + .Select(kvp => kvp.Key) + .ToList(); foreach (var key in completed) { _buildTasks.Remove(key); } - + return _buildTasks.Count < MaxConcurrentBuilds; } @@ -76,7 +93,7 @@ public partial class PlacementManager : Node2D public void Complete(bool wasCancelled = false) { if (IsCompleted) return; - + IsCompleted = true; WasCancelled = wasCancelled; onCompleted?.Invoke(); @@ -158,7 +175,7 @@ public partial class PlacementManager : Node2D public override void _Process(double delta) { if (!Enabled) return; - + // Snap mouse to grid var mousePos = GetGlobalMousePosition(); var newHoveredCell = GridUtils.WorldToGrid(mousePos); @@ -194,24 +211,25 @@ public partial class PlacementManager : Node2D _ghostBuilding.SetGhostMode(canPlace); // Left click to place - if (Input.IsActionPressed("build_tile") && canPlace) + if (Input.IsActionPressed("build_tile")) { var building = Registry.GetBuilding(_currentBuildingId); if (building == null) return; - + if (!CanStartNewBuild()) { - // Optionally show feedback to player that build queue is full + _notReadySound.Play(); return; } // Consume resources first if (!ConsumeBuildingResources(_currentBuildingId)) { - // Optionally show feedback to player that they can't afford this building + _insufficientFundsSound.Play(); return; } + // Create the building instance first var scene = building.Scene; var buildingInstance = (BaseTile)scene.Instantiate(); buildingInstance.RotationDegrees = _currentRotation; @@ -219,23 +237,30 @@ public partial class PlacementManager : Node2D buildingInstance.Position = _ghostBuilding.Position; AddChild(buildingInstance); - // Check if area is free before placing + // First check if area is free if (!IsAreaFree(_hoveredCell, building.Size, _currentRotation, building.Layer)) { + _cannotDeploySound.Play(); RefundBuildingResources(_currentBuildingId); buildingInstance.QueueFree(); return; } - // Occupy the area + // 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; - - buildingInstance.StartConstruction(building.BuildTime, () => { + + // 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)) { @@ -245,6 +270,7 @@ public partial class PlacementManager : Node2D Grid.FreeArea(_hoveredCell, building.Size, _currentRotation, building.Layer); buildingInstance.QueueFree(); } + task.Complete(); _buildTasks.Remove(buildingInstance); } @@ -258,9 +284,28 @@ public partial class PlacementManager : Node2D // 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; + + // 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); } @@ -278,7 +323,7 @@ public partial class PlacementManager : Node2D if (@event.IsActionPressed(ToggleBuildAction)) { Enabled = !Enabled; - + // Hide ghost building when disabling if (!Enabled && _ghostBuilding != null && _ghostBuilding.IsInsideTree()) { @@ -297,41 +342,41 @@ public partial class PlacementManager : Node2D 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) { diff --git a/Scripts/Tiles/BaseTile.cs b/Scripts/Tiles/BaseTile.cs index de19fff..dd523d4 100644 --- a/Scripts/Tiles/BaseTile.cs +++ b/Scripts/Tiles/BaseTile.cs @@ -14,6 +14,7 @@ public partial class BaseTile : Node2D private Sprite2D _sprite; private ColorRect _progressOverlay; private Action _onConstructionComplete; + private bool _isConstructing = false; public override void _Ready() { @@ -26,6 +27,9 @@ public partial class BaseTile : Node2D public void SetGhostMode(bool canPlace) { + // Don't modify collision for constructing buildings + if (_isConstructing) return; + if (_collisionShape != null) _collisionShape.Disabled = true; @@ -46,8 +50,13 @@ public partial class BaseTile : Node2D // Building progress visualization public void StartConstruction(float buildTime, Action onComplete = null) { + _isConstructing = true; + if (_collisionShape != null) + _collisionShape.Disabled = true; + if (_progressOverlay == null || _sprite?.Texture == null) { + _isConstructing = false; onComplete?.Invoke(); return; } @@ -55,6 +64,10 @@ public partial class BaseTile : Node2D _onConstructionComplete = onComplete; var texSize = new Vector2(GridUtils.TileSize, GridUtils.TileSize); + // Set initial transparency for construction + if (_sprite != null) + _sprite.Modulate = new Color(1, 1, 1, 0.8f); + _progressOverlay.Visible = true; _progressOverlay.Modulate = Colors.White; _progressOverlay.Color = new Color(0, 0, 1, 0.4f); // semi-transparent blue @@ -80,7 +93,14 @@ public partial class BaseTile : Node2D // Fade out the overlay await FadeOutOverlay(0.5f); - // Notify completion + // Construction complete - restore full opacity and enable collision + if (_sprite != null) + _sprite.Modulate = Colors.White; + + _isConstructing = false; + if (_collisionShape != null) + _collisionShape.Disabled = false; + _onConstructionComplete?.Invoke(); } diff --git a/Sounds/Events/Building.wav b/Sounds/Events/Building.wav new file mode 100644 index 0000000..e55f0ab Binary files /dev/null and b/Sounds/Events/Building.wav differ diff --git a/Sounds/Events/Building.wav.import b/Sounds/Events/Building.wav.import new file mode 100644 index 0000000..8188410 --- /dev/null +++ b/Sounds/Events/Building.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://d1trbqrntmuij" +path="res://.godot/imported/Building.wav-b8766581fd25a206c63f47b13bd2e2f5.sample" + +[deps] + +source_file="res://Sounds/Events/Building.wav" +dest_files=["res://.godot/imported/Building.wav-b8766581fd25a206c63f47b13bd2e2f5.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/Sounds/Events/Canceled.wav b/Sounds/Events/Canceled.wav new file mode 100644 index 0000000..bf9530c Binary files /dev/null and b/Sounds/Events/Canceled.wav differ diff --git a/Sounds/Events/Canceled.wav.import b/Sounds/Events/Canceled.wav.import new file mode 100644 index 0000000..0182b0b --- /dev/null +++ b/Sounds/Events/Canceled.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://chn8ux4two1kd" +path="res://.godot/imported/Canceled.wav-6d441d9a898b5cd9be4c0664a1f489ed.sample" + +[deps] + +source_file="res://Sounds/Events/Canceled.wav" +dest_files=["res://.godot/imported/Canceled.wav-6d441d9a898b5cd9be4c0664a1f489ed.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/Sounds/Events/CannotDeployHere.wav b/Sounds/Events/CannotDeployHere.wav new file mode 100644 index 0000000..07bd297 Binary files /dev/null and b/Sounds/Events/CannotDeployHere.wav differ diff --git a/Sounds/Events/CannotDeployHere.wav.import b/Sounds/Events/CannotDeployHere.wav.import new file mode 100644 index 0000000..a6af0b1 --- /dev/null +++ b/Sounds/Events/CannotDeployHere.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://7u1gw7lt5xd1" +path="res://.godot/imported/CannotDeployHere.wav-a9ec27508654f74d03ac7b8c8037059c.sample" + +[deps] + +source_file="res://Sounds/Events/CannotDeployHere.wav" +dest_files=["res://.godot/imported/CannotDeployHere.wav-a9ec27508654f74d03ac7b8c8037059c.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/Sounds/Events/InsufficientFunds.wav b/Sounds/Events/InsufficientFunds.wav new file mode 100644 index 0000000..1e02ca0 Binary files /dev/null and b/Sounds/Events/InsufficientFunds.wav differ diff --git a/Sounds/Events/InsufficientFunds.wav.import b/Sounds/Events/InsufficientFunds.wav.import new file mode 100644 index 0000000..44cd608 --- /dev/null +++ b/Sounds/Events/InsufficientFunds.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://bemxvqcettqgp" +path="res://.godot/imported/InsufficientFunds.wav-7aba215cb1cd04a5285a5e9908999906.sample" + +[deps] + +source_file="res://Sounds/Events/InsufficientFunds.wav" +dest_files=["res://.godot/imported/InsufficientFunds.wav-7aba215cb1cd04a5285a5e9908999906.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/Sounds/Events/NotReady.wav b/Sounds/Events/NotReady.wav new file mode 100644 index 0000000..02cb264 Binary files /dev/null and b/Sounds/Events/NotReady.wav differ diff --git a/Sounds/Events/NotReady.wav.import b/Sounds/Events/NotReady.wav.import new file mode 100644 index 0000000..b5222b2 --- /dev/null +++ b/Sounds/Events/NotReady.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dogbl6tfealwg" +path="res://.godot/imported/NotReady.wav-2cfa5f22feed110ca411d6478060fdea.sample" + +[deps] + +source_file="res://Sounds/Events/NotReady.wav" +dest_files=["res://.godot/imported/NotReady.wav-2cfa5f22feed110ca411d6478060fdea.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2