Files
2026-06-22 16:18:34 +02:00

212 lines
7.2 KiB
C#

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using DG.Tweening;
using System;
using System.Collections.Generic;
using Ashwild.Inventory;
namespace Ashwild.Crafting
{
/// <summary>
/// Pure crafting view: shows the recipe list, the detail panel and the craft button.
/// It knows nothing about the player or the crafting logic — CraftingManager drives it and
/// passes in the player-dependent queries (can-craft, item-count) and the craft action as
/// delegates. The UI never reaches back into the player or the manager.
/// </summary>
public class CraftingUI : MonoBehaviour
{
#region Serialized Fields
[Header("Recipe List (Left)")]
[SerializeField] private Transform recipeListContainer;
[SerializeField] private GameObject recipeButtonPrefab;
[Header("Detail Panel (Right)")]
[SerializeField] private GameObject detailPanel;
[SerializeField] private Image resultIcon;
[SerializeField] private TextMeshProUGUI resultNameText;
[SerializeField] private TextMeshProUGUI resultQuantityText;
[SerializeField] private Transform ingredientContainer;
[SerializeField] private GameObject ingredientSlotPrefab;
[SerializeField] private Button craftButton;
[Header("Craft Feedback")]
[SerializeField] private float punchScale = 0.15f;
[SerializeField] private float punchDuration = 0.3f;
#endregion
#region State
private Func<CraftingRecipe, int, bool> canCraftQuery;
private Func<ItemData, int> countItemQuery;
private Func<CraftingRecipe, int, bool> craftRequest;
private readonly List<RecipeButtonUI> recipeButtons = new List<RecipeButtonUI>();
private readonly List<GameObject> ingredientInstances = new List<GameObject>();
private RecipeButtonUI selectedButton;
private Tweener punchTween;
private bool built;
#endregion
#region Public API
/// <summary>
/// Builds the recipe buttons (once) and wires the manager-owned queries/action. Safe to
/// call again later to re-point the delegates; it then simply refreshes the display.
/// </summary>
public void Build(CraftingRecipe[] recipes, Func<CraftingRecipe, int, bool> canCraft, Func<ItemData, int> countItem, Func<CraftingRecipe, int, bool> craft)
{
canCraftQuery = canCraft;
countItemQuery = countItem;
craftRequest = craft;
if (!built)
{
for (int i = 0; i < recipes.Length; i++)
{
GameObject btnGO = Instantiate(recipeButtonPrefab, recipeListContainer);
RecipeButtonUI btn = btnGO.GetComponent<RecipeButtonUI>();
btn.Initialize(recipes[i], OnRecipeSelected, OnCountChanged);
recipeButtons.Add(btn);
}
craftButton.onClick.AddListener(OnCraftClicked);
detailPanel.SetActive(false);
built = true;
}
Refresh();
}
/// <summary>
/// Recomputes craftability for every recipe button and refreshes the open detail.
/// Called by the manager whenever the local player's inventory changes.
/// </summary>
public void Refresh()
{
if (!built) return;
for (int i = 0; i < recipeButtons.Count; i++)
{
RecipeButtonUI btn = recipeButtons[i];
bool canCraft = canCraftQuery != null && canCraftQuery(btn.Recipe, btn.CraftCount);
btn.UpdateCraftability(canCraft);
}
if (selectedButton != null)
RefreshDetail();
}
#endregion
#region Unity Lifecycle
/// <summary>
/// Kills the feedback tween so it never targets a destroyed button.
/// </summary>
private void OnDestroy()
{
punchTween?.Kill();
}
#endregion
#region Event Handlers
/// <summary>
/// Selects a recipe and opens its detail panel.
/// </summary>
private void OnRecipeSelected(RecipeButtonUI button)
{
if (selectedButton == button) return;
if (selectedButton != null)
selectedButton.SetSelected(false);
selectedButton = button;
selectedButton.SetSelected(true);
detailPanel.SetActive(true);
RefreshDetail();
}
/// <summary>
/// Refreshes the detail panel when the selected recipe's craft count changes.
/// </summary>
private void OnCountChanged(RecipeButtonUI button)
{
if (button == selectedButton)
RefreshDetail();
}
/// <summary>
/// Asks the manager to craft the selected recipe and plays feedback on success.
/// </summary>
private void OnCraftClicked()
{
if (selectedButton == null || craftRequest == null) return;
if (craftRequest(selectedButton.Recipe, selectedButton.CraftCount))
{
punchTween?.Kill();
punchTween = craftButton.transform.DOPunchScale(Vector3.one * punchScale, punchDuration, 5, 0.5f)
.SetUpdate(true);
selectedButton.ResetCount();
}
}
#endregion
#region Internal Helpers
/// <summary>
/// Renders the detail panel (result + ingredient slots) for the selected recipe,
/// using the manager-provided item-count query to show what the player owns.
/// </summary>
private void RefreshDetail()
{
CraftingRecipe recipe = selectedButton.Recipe;
int count = selectedButton.CraftCount;
// Result info
resultIcon.sprite = recipe.Icon;
resultNameText.text = recipe.RecipeName;
int totalResult = recipe.ResultQuantity * count;
bool showQty = totalResult > 1;
resultQuantityText.gameObject.SetActive(showQty);
if (showQty)
resultQuantityText.text = "x" + totalResult;
// Clear old ingredients
foreach (GameObject go in ingredientInstances)
Destroy(go);
ingredientInstances.Clear();
// Create ingredient slots
bool canCraft = true;
CraftingIngredient[] ingredients = recipe.Ingredients;
for (int i = 0; i < ingredients.Length; i++)
{
GameObject slotGO = Instantiate(ingredientSlotPrefab, ingredientContainer);
IngredientSlotUI slotUI = slotGO.GetComponent<IngredientSlotUI>();
int required = ingredients[i].quantity * count;
int playerHas = countItemQuery != null ? countItemQuery(ingredients[i].item) : 0;
slotUI.Setup(ingredients[i].item, required, playerHas);
if (playerHas < required)
canCraft = false;
ingredientInstances.Add(slotGO);
}
craftButton.interactable = canCraft;
}
#endregion
}
}