✨ Complete resource manager
This commit is contained in:
		| @@ -1,6 +1,7 @@ | ||||
| [gd_scene load_steps=6 format=3 uid="uid://c22aprj452aha"] | ||||
| [gd_scene load_steps=7 format=3 uid="uid://c22aprj452aha"] | ||||
|  | ||||
| [ext_resource type="Script" uid="uid://cudpc3w17mbsw" path="res://Scripts/System/GridManager.cs" id="1_knkkn"] | ||||
| [ext_resource type="Script" uid="uid://dfi2snip78eq6" path="res://Scripts/System/ResourceManager.cs" id="1_pl8e4"] | ||||
| [ext_resource type="Script" uid="uid://cfbj72nm0eovg" path="res://Scripts/System/BuildingRegistry.cs" id="1_sxhdm"] | ||||
| [ext_resource type="Script" uid="uid://cugfbvw70clgd" path="res://Scripts/System/NaturalResourceGenerator.cs" id="2_oss8w"] | ||||
| [ext_resource type="Script" uid="uid://bx1wj7gn6vrqe" path="res://Scripts/System/PlacementManager.cs" id="2_sxhdm"] | ||||
| @@ -8,6 +9,9 @@ | ||||
|  | ||||
| [node name="Root" type="Node2D"] | ||||
|  | ||||
| [node name="ResourceSystem" type="Node" parent="."] | ||||
| script = ExtResource("1_pl8e4") | ||||
|  | ||||
| [node name="BuildingRegistry" type="Node" parent="."] | ||||
| script = ExtResource("1_sxhdm") | ||||
|  | ||||
| @@ -19,9 +23,10 @@ Registry = NodePath("../BuildingRegistry") | ||||
| [node name="GridSystem" type="Node2D" parent="."] | ||||
| script = ExtResource("1_knkkn") | ||||
|  | ||||
| [node name="PlacementSystem" type="Node2D" parent="." node_paths=PackedStringArray("Grid", "Registry")] | ||||
| [node name="PlacementSystem" type="Node2D" parent="." node_paths=PackedStringArray("Grid", "Inventory", "Registry")] | ||||
| script = ExtResource("2_sxhdm") | ||||
| Grid = NodePath("../GridSystem") | ||||
| Inventory = NodePath("../ResourceSystem") | ||||
| Registry = NodePath("../BuildingRegistry") | ||||
|  | ||||
| [node name="Player" parent="." instance=ExtResource("3_oss8w")] | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
| using Godot; | ||||
| using static System.Int32; | ||||
|  | ||||
| namespace AceFieldNewHorizon.Scripts.System; | ||||
|  | ||||
| @@ -101,12 +102,22 @@ public partial class BuildingRegistry : Node | ||||
| 				{ | ||||
| 					int val; | ||||
| 					var obj = costDict[mat]; | ||||
| 					if (obj.VariantType == Variant.Type.PackedInt64Array) | ||||
| 						val = (int)obj.AsInt64(); | ||||
| 					else if (obj.VariantType == Variant.Type.PackedInt32Array) | ||||
| 						val = obj.AsInt32(); | ||||
| 					else | ||||
| 						int.TryParse(obj.ToString(), out val); | ||||
| 					switch (obj.VariantType) | ||||
| 					{ | ||||
| 						case Variant.Type.PackedInt64Array: | ||||
| 							val = (int)obj.AsInt64(); | ||||
| 							break; | ||||
| 						case Variant.Type.PackedInt32Array: | ||||
| 							val = obj.AsInt32(); | ||||
| 							break; | ||||
| 						case Variant.Type.Float: | ||||
| 							val = (int)obj.AsDouble(); | ||||
| 							break; | ||||
| 						default: | ||||
| 							if (!TryParse(obj.ToString(), out val)) | ||||
| 								GD.PrintErr($"[BuildingRegistry] Failed to parse cost for '{key}': {obj.ToString()}"); | ||||
| 							break; | ||||
| 					} | ||||
| 					cost[mat] = val; | ||||
| 				} | ||||
| 			} | ||||
| @@ -120,7 +131,7 @@ public partial class BuildingRegistry : Node | ||||
| 				else if (dObj.VariantType == Variant.Type.PackedInt32Array) | ||||
| 					durability = dObj.AsInt32(); | ||||
| 				else | ||||
| 					int.TryParse(dObj.ToString(), out durability); | ||||
| 					TryParse(dObj.ToString(), out durability); | ||||
| 			} | ||||
|  | ||||
| 			// Parse buildTime | ||||
|   | ||||
| @@ -9,11 +9,12 @@ 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<string> BuildableTiles = ["wall", "miner"]; | ||||
|     private readonly List<BuildTask> _activeBuilds = new(); | ||||
|     private readonly Dictionary<Node2D, BuildTask> _buildTasks = new(); | ||||
|     private AudioStreamPlayer _completionSound; | ||||
|  | ||||
|     public override void _Ready() | ||||
| @@ -33,10 +34,16 @@ public partial class PlacementManager : Node2D | ||||
|     private void OnBuildCompleted() | ||||
|     { | ||||
|         // Remove all completed builds | ||||
|         _activeBuilds.RemoveAll(task => task.IsCompleted); | ||||
|         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 (_activeBuilds.Count == 0) | ||||
|         if (_buildTasks.Count == 0) | ||||
|         { | ||||
|             _completionSound.Play(); | ||||
|         } | ||||
| @@ -46,30 +53,34 @@ public partial class PlacementManager : Node2D | ||||
|     private bool CanStartNewBuild() | ||||
|     { | ||||
|         // Remove completed builds | ||||
|         _activeBuilds.RemoveAll(task => task.IsCompleted); | ||||
|         return _activeBuilds.Count < MaxConcurrentBuilds; | ||||
|         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 : IDisposable | ||||
|     private class BuildTask(Action onCompleted) : IDisposable | ||||
|     { | ||||
|         private readonly Action _onCompleted; | ||||
|         public bool IsCompleted { get; private set; } | ||||
|         public bool WasCancelled { get; private set; } | ||||
|  | ||||
|         public BuildTask(Action onCompleted) | ||||
|         { | ||||
|             _onCompleted = onCompleted; | ||||
|         } | ||||
|  | ||||
|         public void Complete() | ||||
|         public void Complete(bool wasCancelled = false) | ||||
|         { | ||||
|             if (IsCompleted) return; | ||||
|              | ||||
|             IsCompleted = true; | ||||
|             _onCompleted?.Invoke(); | ||||
|             WasCancelled = wasCancelled; | ||||
|             onCompleted?.Invoke(); | ||||
|         } | ||||
|  | ||||
|         public void Dispose() | ||||
|         { | ||||
|             Complete(); | ||||
|             Complete(true); // Mark as cancelled if disposed before completion | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -188,6 +199,13 @@ public partial class PlacementManager : Node2D | ||||
|                 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; | ||||
| @@ -195,13 +213,36 @@ public partial class PlacementManager : Node2D | ||||
|             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); | ||||
|                 _activeBuilds.Add(buildTask); | ||||
|                 buildingInstance.StartConstruction(building.BuildTime, buildTask.Complete); | ||||
|                 _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); | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -226,14 +267,64 @@ public partial class PlacementManager : Node2D | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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 rotation is allowed | ||||
|         if (!buildingData.IsRotationAllowed(_currentRotation)) | ||||
|             return false; | ||||
|         // Check if we can afford the building | ||||
|         if (!CanAffordBuilding(_currentBuildingId)) return false; | ||||
|  | ||||
|         // Check if area is free | ||||
|         var rotatedSize = buildingData.GetRotatedSize(_currentRotation); | ||||
| @@ -250,6 +341,11 @@ public partial class PlacementManager : Node2D | ||||
|             _ => [] | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private bool IsAreaFree(Vector2I topLeft, Vector2I size, float rotation, GridLayer layer) | ||||
|     { | ||||
|         return !Grid.IsAreaOccupied(topLeft, size, rotation, GetBlockingLayers(layer)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| public static class GridManagerExtensions | ||||
|   | ||||
							
								
								
									
										145
									
								
								Scripts/System/ResourceManager.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								Scripts/System/ResourceManager.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| using Godot; | ||||
| using System.Collections.Generic; | ||||
| using System.Linq; | ||||
|  | ||||
| namespace AceFieldNewHorizon.Scripts.System; | ||||
|  | ||||
| public partial class ResourceManager : Node | ||||
| { | ||||
|     // Dictionary to store resources with their IDs and quantities | ||||
|     private readonly Dictionary<string, int> _resources = new(); | ||||
|     private static readonly StringName LoggerName = "ResourceManager"; | ||||
|  | ||||
|     // Event for when resource amounts change | ||||
|     [Signal] | ||||
|     public delegate void OnResourceChangedEventHandler(string resourceId, int newAmount); | ||||
|  | ||||
|     public override void _Ready() | ||||
|     { | ||||
|         base._Ready(); | ||||
|         GD.Print($"[{LoggerName}] ResourceManager initialized"); | ||||
|     } | ||||
|  | ||||
|     // Add resources of a specific type | ||||
|     public void AddResource(string resourceId, int amount = 1) | ||||
|     { | ||||
|         if (amount <= 0)  | ||||
|         { | ||||
|             GD.PushWarning($"[{LoggerName}] Attempted to add non-positive amount ({amount}) for resource: {resourceId}"); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         bool isNew = !_resources.ContainsKey(resourceId); | ||||
|          | ||||
|         if (!_resources.TryAdd(resourceId, amount)) | ||||
|         { | ||||
|             var oldAmount = _resources[resourceId]; | ||||
|             _resources[resourceId] += amount; | ||||
|             GD.Print($"[{LoggerName}] Added {amount} {resourceId}. New total: {_resources[resourceId]} (was {oldAmount})"); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             GD.Print($"[{LoggerName}] Added new resource: {resourceId} with amount: {amount}"); | ||||
|         } | ||||
|  | ||||
|         EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]); | ||||
|     } | ||||
|  | ||||
|     // Remove resources of a specific type | ||||
|     public bool RemoveResource(string resourceId, int amount = 1) | ||||
|     { | ||||
|         if (amount <= 0)  | ||||
|         { | ||||
|             GD.PushWarning($"[{LoggerName}] Attempted to remove non-positive amount ({amount}) for resource: {resourceId}"); | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|         if (!_resources.ContainsKey(resourceId)) | ||||
|         { | ||||
|             GD.Print($"[{LoggerName}] Failed to remove {amount} {resourceId}: Resource not found"); | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|         if (_resources[resourceId] < amount) | ||||
|         { | ||||
|             GD.Print($"[{LoggerName}] Insufficient {resourceId}. Requested: {amount}, Available: {_resources[resourceId]}"); | ||||
|             return false; | ||||
|         } | ||||
|          | ||||
|         var oldAmount = _resources[resourceId]; | ||||
|         _resources[resourceId] -= amount; | ||||
|          | ||||
|         GD.Print($"[{LoggerName}] Removed {amount} {resourceId}. New total: {_resources[resourceId]} (was {oldAmount})"); | ||||
|          | ||||
|         // Remove the entry if quantity reaches zero | ||||
|         if (_resources[resourceId] <= 0) | ||||
|         { | ||||
|             _resources.Remove(resourceId); | ||||
|             GD.Print($"[{LoggerName}] Removed resource entry for {resourceId} (reached zero)"); | ||||
|             EmitSignal(nameof(OnResourceChanged), resourceId, 0); | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             EmitSignal(nameof(OnResourceChanged), resourceId, _resources[resourceId]); | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     // Get the current amount of a specific resource | ||||
|     public int GetResourceAmount(string resourceId) | ||||
|     { | ||||
|         // Silently return 0 for non-existent resources | ||||
|         return _resources.GetValueOrDefault(resourceId, 0); | ||||
|     } | ||||
|  | ||||
|     // Check if there are at least 'amount' of a specific resource | ||||
|     public bool HasResource(string resourceId, int amount = 1) | ||||
|     { | ||||
|         if (amount <= 0) return true; // Always true for zero or negative amounts | ||||
|          | ||||
|         var exists = _resources.TryGetValue(resourceId, out var currentAmount); | ||||
|         var hasEnough = exists && currentAmount >= amount; | ||||
|          | ||||
|         return hasEnough; | ||||
|     } | ||||
|  | ||||
|     // Get all resources as a read-only dictionary | ||||
|     public IReadOnlyDictionary<string, int> GetAllResources() | ||||
|     { | ||||
|         GD.Print($"[{LoggerName}] Getting all resources. Total types: {_resources.Count}"); | ||||
|         return new Dictionary<string, int>(_resources); | ||||
|     } | ||||
|  | ||||
|     // Clear all resources | ||||
|     public void Clear() | ||||
|     { | ||||
|         GD.Print($"[{LoggerName}] Clearing all resources. Total types before clear: {_resources.Count}"); | ||||
|          | ||||
|         var keys = new List<string>(_resources.Keys); | ||||
|         foreach (var resourceId in keys) | ||||
|         { | ||||
|             _resources[resourceId] = 0; | ||||
|             EmitSignal(nameof(OnResourceChanged), resourceId, 0); | ||||
|         } | ||||
|         _resources.Clear(); | ||||
|          | ||||
|         GD.Print($"[{LoggerName}] All resources cleared"); | ||||
|     } | ||||
|  | ||||
|     // Helper method to log current resource state | ||||
|     public void LogResourceState() | ||||
|     { | ||||
|         if (_resources.Count == 0) | ||||
|         { | ||||
|             GD.Print($"[{LoggerName}] No resources currently stored"); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         GD.Print($"[{LoggerName}] Current resource state:"); | ||||
|         foreach (var (id, amount) in _resources.OrderBy(x => x.Key)) | ||||
|         { | ||||
|             GD.Print($"  - {id}: {amount}"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										1
									
								
								Scripts/System/ResourceManager.cs.uid
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								Scripts/System/ResourceManager.cs.uid
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| uid://dfi2snip78eq6 | ||||
		Reference in New Issue
	
	Block a user