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
}
}