using UnityEngine; using UnityEngine.Events; using Ashwild.Inventory; using Ashwild.Player; namespace Ashwild.Crafting { /// /// Owns the crafting recipes and logic, and drives the (dumb) CraftingUI: it builds the view /// from recipe data, hands it the player-dependent queries, and refreshes it whenever the /// local player's inventory changes. The UI never reaches back into the player or this class. /// Keep this and CraftingUI as stable scene objects so the reference can't break. /// public class CraftingManager : MonoBehaviour { #region Serialized Fields [Header("Data")] [SerializeField] private CraftingRecipe[] recipes; [Header("View")] [Tooltip("The crafting panel this manager drives. Both should be stable scene objects.")] [SerializeField] private CraftingUI craftingUI; [Header("Events")] public UnityEvent onCraftSuccess; #endregion #region State private PlayerInventory inventory; private bool bound; #endregion #region Public API /// /// All authored recipes (read-only data source). /// public CraftingRecipe[] Recipes => recipes; /// /// Returns whether the local player can currently craft the recipe the given number of times. /// public bool CanCraft(CraftingRecipe recipe, int count = 1) { return PlayerInventory.Instance != null && recipe.CanCraft(PlayerInventory.Instance, count); } /// /// Consumes the ingredients and produces the result if the local player can afford it. /// public bool TryCraft(CraftingRecipe recipe, int count = 1) { PlayerInventory inv = PlayerInventory.Instance; if (inv == null || !recipe.CanCraft(inv, count)) return false; // Consume ingredients for (int i = 0; i < recipe.Ingredients.Length; i++) { CraftingIngredient ingredient = recipe.Ingredients[i]; inv.RemoveItemByData(ingredient.item, ingredient.quantity * count); } // Produce result inv.AddItem(recipe.ResultItem, recipe.ResultQuantity * count); onCraftSuccess?.Invoke(recipe); return true; } #endregion #region Unity Lifecycle /// /// Subscribes to the local player spawn so we can bind to its inventory. /// private void OnEnable() { PlayerEvents.LocalPlayerSpawned += HandleLocalPlayerSpawned; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned; } /// /// Builds the view from static recipe data, then binds to the player if it already exists. /// private void Start() { if (craftingUI != null) craftingUI.Build(recipes, CanCraft, CountItem, TryCraft); else Debug.LogError("[CraftingManager] No CraftingUI assigned — the crafting panel will not populate.", this); // The player may already exist (late init); otherwise we wait for the spawn event. if (PlayerInventory.Instance != null) BindToInventory(); } /// /// Stops listening to the inventory when destroyed. /// private void OnDestroy() { if (inventory != null) inventory.onSlotChanged.RemoveListener(HandleInventoryChanged); } #endregion #region Event Handlers /// /// Binds to the inventory when the local player spawns on the network. /// private void HandleLocalPlayerSpawned() => BindToInventory(); /// /// Refreshes the view's craftability whenever any inventory slot changes. /// private void HandleInventoryChanged(int slotIndex) { if (craftingUI != null) craftingUI.Refresh(); } #endregion #region Internal Helpers /// /// Caches the local player's inventory and starts reacting to its changes; runs once. /// private void BindToInventory() { if (bound) return; inventory = PlayerInventory.Instance; if (inventory == null) return; bound = true; inventory.onSlotChanged.AddListener(HandleInventoryChanged); if (craftingUI != null) craftingUI.Refresh(); } /// /// How many of an item the local player currently holds (0 when offline). /// private int CountItem(ItemData item) { return PlayerInventory.Instance != null ? PlayerInventory.Instance.CountItem(item) : 0; } #endregion } }