212 lines
7.2 KiB
C#
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
|
|
}
|
|
}
|