From 850628ca72ca5bd9917fe767f90e1eefa534c345 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 29 Aug 2025 00:10:19 +0800 Subject: [PATCH] :sparkles: Complete resource manager --- Scenes/Root.tscn | 9 +- Scripts/System/BuildingRegistry.cs | 25 +++-- Scripts/System/PlacementManager.cs | 136 ++++++++++++++++++++---- Scripts/System/ResourceManager.cs | 145 ++++++++++++++++++++++++++ Scripts/System/ResourceManager.cs.uid | 1 + 5 files changed, 287 insertions(+), 29 deletions(-) create mode 100644 Scripts/System/ResourceManager.cs create mode 100644 Scripts/System/ResourceManager.cs.uid diff --git a/Scenes/Root.tscn b/Scenes/Root.tscn index 05c400b..0d73d55 100644 --- a/Scenes/Root.tscn +++ b/Scenes/Root.tscn @@ -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")] diff --git a/Scripts/System/BuildingRegistry.cs b/Scripts/System/BuildingRegistry.cs index e54c980..64b800a 100644 --- a/Scripts/System/BuildingRegistry.cs +++ b/Scripts/System/BuildingRegistry.cs @@ -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 diff --git a/Scripts/System/PlacementManager.cs b/Scripts/System/PlacementManager.cs index 7bbcb45..d96c459 100644 --- a/Scripts/System/PlacementManager.cs +++ b/Scripts/System/PlacementManager.cs @@ -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 BuildableTiles = ["wall", "miner"]; - private readonly List _activeBuilds = new(); + private readonly Dictionary _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 diff --git a/Scripts/System/ResourceManager.cs b/Scripts/System/ResourceManager.cs new file mode 100644 index 0000000..9918bee --- /dev/null +++ b/Scripts/System/ResourceManager.cs @@ -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 _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 GetAllResources() + { + GD.Print($"[{LoggerName}] Getting all resources. Total types: {_resources.Count}"); + return new Dictionary(_resources); + } + + // Clear all resources + public void Clear() + { + GD.Print($"[{LoggerName}] Clearing all resources. Total types before clear: {_resources.Count}"); + + var keys = new List(_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}"); + } + } +} \ No newline at end of file diff --git a/Scripts/System/ResourceManager.cs.uid b/Scripts/System/ResourceManager.cs.uid new file mode 100644 index 0000000..a8f123a --- /dev/null +++ b/Scripts/System/ResourceManager.cs.uid @@ -0,0 +1 @@ +uid://dfi2snip78eq6