using System.Collections.Generic; 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; private readonly HashSet unlockedRecipes = new HashSet(); #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. /// A recipe is only craftable once every ingredient has been discovered, on top of the usual /// inventory check — an ingredient still shown as a mystery ("?") blocks the craft. /// public bool CanCraft(CraftingRecipe recipe, int count = 1) { return PlayerInventory.Instance != null && IsFullyDiscovered(recipe) && recipe.CanCraft(PlayerInventory.Instance, count); } /// /// Consumes the ingredients and produces the result if the local player can afford it and has /// discovered every ingredient. The result item is added through the inventory, which in turn /// discovers it — so crafting an intermediate result reveals the recipes further down the chain. /// public bool TryCraft(CraftingRecipe recipe, int count = 1) { PlayerInventory inv = PlayerInventory.Instance; if (inv == null || !IsFullyDiscovered(recipe) || !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; PlayerEvents.ItemDiscovered += HandleItemDiscovered; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned; PlayerEvents.ItemDiscovered -= HandleItemDiscovered; } /// /// 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, IsDiscovered, IsRecipeVisible); 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(); } /// /// Reacts to a newly discovered item: announces any recipe it just unlocked (for the "NEW" /// craft notification), then refreshes the view so those recipes appear and their once-mystery /// ingredients are shown. /// private void HandleItemDiscovered(ItemData item) { NotifyNewlyUnlockedRecipes(); 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); SeedUnlockedRecipes(); if (craftingUI != null) craftingUI.Refresh(); } /// /// Records the recipes already revealed at bind time (e.g. from restored discoveries) so they /// are treated as known and never fire a spurious "NEW" notification. With a fresh player this /// is empty since discovery starts at zero. /// private void SeedUnlockedRecipes() { for (int i = 0; i < recipes.Length; i++) if (IsRecipeVisible(recipes[i])) unlockedRecipes.Add(recipes[i]); } /// /// Raises a "NEW" notification for every recipe that has just become visible (its first /// discovered ingredient), each announced once. Called after an item is discovered. /// private void NotifyNewlyUnlockedRecipes() { for (int i = 0; i < recipes.Length; i++) { CraftingRecipe recipe = recipes[i]; if (unlockedRecipes.Contains(recipe)) continue; if (!IsRecipeVisible(recipe)) continue; unlockedRecipes.Add(recipe); PlayerEvents.RaiseRecipeDiscovered(recipe.Icon, recipe.RecipeName); } } /// /// 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; } /// /// Whether the local player has discovered the item (false when the discovery tracker isn't ready). /// private bool IsDiscovered(ItemData item) { return CraftDiscovery.Instance != null && CraftDiscovery.Instance.IsDiscovered(item); } /// /// Whether the recipe should appear at all: revealed as soon as at least one of its ingredients /// has been discovered. Undiscovered ingredients then show as a mystery until found. /// private bool IsRecipeVisible(CraftingRecipe recipe) { CraftingIngredient[] ingredients = recipe.Ingredients; for (int i = 0; i < ingredients.Length; i++) if (IsDiscovered(ingredients[i].item)) return true; return false; } /// /// Whether every ingredient of the recipe has been discovered — the gate for actually crafting it. /// private bool IsFullyDiscovered(CraftingRecipe recipe) { CraftingIngredient[] ingredients = recipe.Ingredients; for (int i = 0; i < ingredients.Length; i++) if (!IsDiscovered(ingredients[i].item)) return false; return true; } #endregion } }