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. /// /// It also owns the crafting *context* — bare hands or a station. Opening the panel from a /// widens the visible catalog to that station's recipes for as long as /// the window stays open; closing it drops back to hand crafting. /// public class CraftingManager : MonoBehaviour { #region Singleton /// /// The scene's crafting manager, so a world station can hand itself over without a /// FindObjectOfType. Mirrors BuildManager. /// public static CraftingManager Instance { get; private set; } #endregion #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(); /// /// The station the panel is currently open on, or None while the player crafts bare-handed /// (the inventory opened with the normal key). Reset whenever the window closes. /// private CraftingStationType activeStation = CraftingStationType.None; #endregion #region Public API /// /// All authored recipes (read-only data source). /// public CraftingRecipe[] Recipes => recipes; /// /// Opens the crafting panel on the given station: its recipes join the catalog and the inventory /// window opens straight on the craft tab. The context lasts until the window closes. /// Called by on the interacting client only. /// public void OpenStation(CraftingStation station) { if (station == null) return; if (InventoryUI.Instance == null) { Debug.LogError("[CraftingManager] No InventoryUI in the scene — cannot open the crafting panel.", this); return; } activeStation = station.StationType; InventoryUI.Instance.OpenAtCategory(InventoryCategory.Crafting); if (craftingUI != null) craftingUI.Refresh(); } /// /// 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 and the right station is /// open, 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 && IsStationAvailable(recipe) && IsFullyDiscovered(recipe) && recipe.CanCraft(PlayerInventory.Instance, count); } /// /// Consumes the ingredients and produces the result if the local player can afford it, has /// discovered every ingredient, and is at the station the recipe requires. 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. /// /// The station is re-checked here rather than trusted from the visible list: the panel could /// have been left open through a context change, and a bench recipe must never be craftable /// bare-handed. /// public bool TryCraft(CraftingRecipe recipe, int count = 1) { PlayerInventory inv = PlayerInventory.Instance; if (inv == null || !IsStationAvailable(recipe) || !IsFullyDiscovered(recipe) || !recipe.CanCraft(inv, count)) return false; for (int i = 0; i < recipe.Ingredients.Length; i++) { CraftingIngredient ingredient = recipe.Ingredients[i]; inv.RemoveItemByData(ingredient.item, ingredient.quantity * count); } inv.AddItem(recipe.ResultItem, recipe.ResultQuantity * count); onCraftSuccess?.Invoke(recipe); return true; } #endregion #region Unity Lifecycle /// /// Registers the singleton so world stations can reach the manager. /// private void Awake() { Instance = this; } /// /// Subscribes to the local player spawn so we can bind to its inventory, and to the window's /// open state so the station context is dropped when it closes. /// private void OnEnable() { PlayerEvents.LocalPlayerSpawned += HandleLocalPlayerSpawned; PlayerEvents.ItemDiscovered += HandleItemDiscovered; PlayerEvents.InventoryOpenChanged += HandleInventoryOpenChanged; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned; PlayerEvents.ItemDiscovered -= HandleItemDiscovered; PlayerEvents.InventoryOpenChanged -= HandleInventoryOpenChanged; } /// /// 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 and clears the singleton when destroyed. /// private void OnDestroy() { if (inventory != null) inventory.onSlotChanged.RemoveListener(HandleInventoryChanged); if (Instance == this) Instance = null; } #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(); } /// /// Drops the station context when the inventory window closes, so the next hand-opened panel is /// back to hand-only recipes. Tying this to the window rather than to a distance check keeps a /// single, unambiguous rule: the station lasts exactly as long as the panel it opened. /// private void HandleInventoryOpenChanged(bool open) { if (open || activeStation == CraftingStationType.None) return; activeStation = CraftingStationType.None; 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 (IsDiscoveryRevealed(recipes[i])) unlockedRecipes.Add(recipes[i]); } /// /// Raises a "NEW" notification for every recipe that has just been revealed (its first /// discovered ingredient), each announced once. Called after an item is discovered. /// /// Deliberately keyed on discovery alone, not on the station filter: a bench recipe must still /// announce itself the moment its ingredient is found — that is the cue telling the player the /// bench is worth building — instead of silently waiting for the bench to be opened. /// private void NotifyNewlyUnlockedRecipes() { for (int i = 0; i < recipes.Length; i++) { CraftingRecipe recipe = recipes[i]; if (unlockedRecipes.Contains(recipe)) continue; if (!IsDiscoveryRevealed(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 in the list right now: revealed by discovery AND allowed by /// the station the panel is open on. The two criteria stay separate helpers on purpose — /// discovery is permanent player progress, the station is a transient context — and only their /// conjunction drives what the player sees. /// private bool IsRecipeVisible(CraftingRecipe recipe) { return IsStationAvailable(recipe) && IsDiscoveryRevealed(recipe); } /// /// Whether the recipe has been revealed by discovery: true as soon as at least one of its /// ingredients has been found. Undiscovered ingredients then show as a mystery until found. /// private bool IsDiscoveryRevealed(CraftingRecipe recipe) { CraftingIngredient[] ingredients = recipe.Ingredients; for (int i = 0; i < ingredients.Length; i++) if (IsDiscovered(ingredients[i].item)) return true; return false; } /// /// Whether the recipe's required station matches the current context: a recipe requiring none is /// craftable everywhere (bare hands and every station alike), any other one only on a station of /// its exact type. /// private bool IsStationAvailable(CraftingRecipe recipe) { return recipe.RequiredStation == CraftingStationType.None || recipe.RequiredStation == activeStation; } /// /// 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 } }