using System; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using Ashwild.Crafting; using Ashwild.Inventory; namespace Ashwild.EditorTools { /// /// The custom editor for a CraftingRecipe shown in the right pane of the Ashwild Database. The /// editor IS the recipe equation: a row of interactive ingredient cards ("+"-joined) leading to /// "=" and the result card. Each card's icon opens an item picker to set/change the item, a /// −/+ stepper adjusts its quantity, and ingredient cards carry a remove button; a dashed "+" tile /// appends a new ingredient. A compact name field sits above it. All edits write straight to the /// asset and repaint the strip. /// public class RecipeEditorView { #region State public VisualElement Root { get; } private const int RecipePickerControlId = 0x41534852; private readonly CraftingRecipe recipe; private readonly SerializedObject so; private readonly SerializedProperty ingredientsProp; private readonly Action onMetaChanged; private VisualElement equation; private Action pickHandler; private bool pickCommitsOnCloseOnly; #endregion #region Construction /// /// Builds the recipe editor tree. is raised when the recipe /// name or result changes so the left list can refresh that row. /// public RecipeEditorView(CraftingRecipe recipe, Action onMetaChanged) { this.recipe = recipe; this.onMetaChanged = onMetaChanged; so = new SerializedObject(recipe); ingredientsProp = so.FindProperty("ingredients"); Root = new VisualElement(); Root.Add(BuildNameField()); Root.Add(BuildEquation()); Root.Add(BuildPickerProxy()); RefreshEquation(); } #endregion #region Header /// /// Builds the recipe-name field shown above the equation, styled as the large editable title /// (same look as an item's hero name field); renaming refreshes the list. /// private VisualElement BuildNameField() { TextField nameField = new TextField(); nameField.AddToClassList("ash-hero__name"); nameField.AddToClassList("ash-recipe-name"); nameField.BindProperty(so.FindProperty("recipeName")); nameField.RegisterValueChangedCallback(_ => onMetaChanged?.Invoke()); return nameField; } #endregion #region Equation /// /// Builds the container for the interactive equation; its cards are filled by RefreshEquation. /// private VisualElement BuildEquation() { equation = new VisualElement(); equation.AddToClassList("ash-equation"); return equation; } /// /// Repaints the whole equation: one interactive card per ingredient (joined by "+"), the dashed /// add tile, then "=", then the result card. /// private void RefreshEquation() { equation.Clear(); int count = ingredientsProp.arraySize; for (int i = 0; i < count; i++) { if (i > 0) equation.Add(Operator("+")); equation.Add(BuildIngredientCard(i)); } if (count > 0) equation.Add(Operator("+")); equation.Add(BuildAddTile()); equation.Add(Operator("=")); equation.Add(BuildResultCard()); } /// /// Builds a large "+"/"=" operator glyph between cards. /// private static Label Operator(string glyph) { Label op = new Label(glyph); op.AddToClassList("ash-equation__op"); return op; } #endregion #region Cards /// /// Builds the interactive card for the ingredient at : a clickable icon /// (change item), the name, a −/+ quantity stepper, and a remove button. /// private VisualElement BuildIngredientCard(int index) { SerializedProperty element = ingredientsProp.GetArrayElementAtIndex(index); ItemData item = element.FindPropertyRelative("item").objectReferenceValue as ItemData; int quantity = element.FindPropertyRelative("quantity").intValue; VisualElement card = MakeCard(item, () => OpenItemPicker(item, true, picked => SetIngredientItem(index, picked))); card.Add(Stepper(quantity, delta => AdjustIngredientQuantity(index, delta))); Button remove = new Button(() => RemoveIngredient(index)) { text = "✕" }; remove.AddToClassList("ash-rchip__remove"); card.Add(remove); return card; } /// /// Builds the result card: a clickable icon (change result item) and a −/+ quantity stepper. /// private VisualElement BuildResultCard() { VisualElement card = MakeCard(recipe.ResultItem, () => OpenItemPicker(recipe.ResultItem, true, SetResultItem)); card.AddToClassList("ash-rchip--result"); card.Add(Stepper(recipe.ResultQuantity, AdjustResultQuantity)); return card; } /// /// Builds the dashed "+" tile that appends a new ingredient once an item is picked. /// private VisualElement BuildAddTile() { VisualElement tile = new VisualElement(); tile.AddToClassList("ash-rchip"); tile.AddToClassList("ash-rchip--add"); tile.tooltip = "Add an ingredient"; tile.RegisterCallback(_ => OpenItemPicker(null, false, AddIngredient)); Label plus = new Label("+"); plus.AddToClassList("ash-rchip__plus"); tile.Add(plus); return tile; } /// /// Builds the shared card body (icon + name) for an item, with the icon wired to open a picker. /// The icon shows the item's sprite, or a neutral placeholder when unset. /// private VisualElement MakeCard(ItemData item, Action onIconClicked) { VisualElement card = new VisualElement(); card.AddToClassList("ash-rchip"); VisualElement icon = new VisualElement(); icon.AddToClassList("ash-rchip__icon"); icon.tooltip = "Click to choose an item"; if (item != null && item.Icon != null) icon.style.backgroundImage = new StyleBackground(item.Icon); icon.RegisterCallback(_ => onIconClicked()); card.Add(icon); Label name = new Label(item != null ? item.ItemName : "Choose…"); name.AddToClassList("ash-rchip__name"); card.Add(name); return card; } /// /// Builds a −/+ quantity stepper showing the current value; receives /// -1 or +1. /// private VisualElement Stepper(int quantity, Action onDelta) { VisualElement stepper = new VisualElement(); stepper.AddToClassList("ash-rchip__stepper"); Button minus = new Button(() => onDelta(-1)) { text = "−" }; minus.AddToClassList("ash-rchip__step"); stepper.Add(minus); Label value = new Label(quantity.ToString()); value.AddToClassList("ash-rchip__qty"); stepper.Add(value); Button plus = new Button(() => onDelta(1)) { text = "+" }; plus.AddToClassList("ash-rchip__step"); stepper.Add(plus); return stepper; } #endregion #region Mutations /// /// Sets the item of an existing ingredient and repaints. /// private void SetIngredientItem(int index, ItemData item) { if (index < 0 || index >= ingredientsProp.arraySize) return; ingredientsProp.GetArrayElementAtIndex(index).FindPropertyRelative("item").objectReferenceValue = item; so.ApplyModifiedProperties(); RefreshEquation(); } /// /// Changes an ingredient's quantity by a delta, clamped to a minimum of one, and repaints. /// private void AdjustIngredientQuantity(int index, int delta) { if (index < 0 || index >= ingredientsProp.arraySize) return; SerializedProperty quantity = ingredientsProp.GetArrayElementAtIndex(index).FindPropertyRelative("quantity"); quantity.intValue = Mathf.Max(1, quantity.intValue + delta); so.ApplyModifiedProperties(); RefreshEquation(); } /// /// Appends a new ingredient (quantity 1) for the picked item and repaints; ignores a null pick /// (e.g. the picker was cancelled). /// private void AddIngredient(ItemData item) { if (item == null) return; int index = ingredientsProp.arraySize; ingredientsProp.arraySize++; SerializedProperty element = ingredientsProp.GetArrayElementAtIndex(index); element.FindPropertyRelative("item").objectReferenceValue = item; element.FindPropertyRelative("quantity").intValue = 1; so.ApplyModifiedProperties(); RefreshEquation(); } /// /// Removes the ingredient at the given index and repaints. /// private void RemoveIngredient(int index) { if (index < 0 || index >= ingredientsProp.arraySize) return; ingredientsProp.DeleteArrayElementAtIndex(index); so.ApplyModifiedProperties(); RefreshEquation(); } /// /// Sets the recipe's result item, repaints, and refreshes the list row (icon/name follow it). /// private void SetResultItem(ItemData item) { so.FindProperty("resultItem").objectReferenceValue = item; so.ApplyModifiedProperties(); RefreshEquation(); onMetaChanged?.Invoke(); } /// /// Changes the result quantity by a delta, clamped to a minimum of one, and repaints. /// private void AdjustResultQuantity(int delta) { SerializedProperty quantity = so.FindProperty("resultQuantity"); quantity.intValue = Mathf.Max(1, quantity.intValue + delta); so.ApplyModifiedProperties(); RefreshEquation(); } #endregion #region Item Picker /// /// Builds the hidden IMGUI proxy that relays Unity's object-picker commands to the active pick /// handler (the editor window is UI Toolkit, which can't receive those commands directly). /// private VisualElement BuildPickerProxy() { IMGUIContainer proxy = new IMGUIContainer(HandlePickerCommands); proxy.style.position = Position.Absolute; proxy.style.width = 1; proxy.style.height = 1; return proxy; } /// /// Opens Unity's item picker seeded with the current item. When /// is true the pick applies on every highlight (live preview, e.g. changing an existing item); /// otherwise it applies only when the picker closes (e.g. adding a new ingredient — commit once). /// private void OpenItemPicker(ItemData seed, bool commitLive, Action onPicked) { pickHandler = onPicked; pickCommitsOnCloseOnly = !commitLive; EditorGUIUtility.ShowObjectPicker(seed, false, string.Empty, RecipePickerControlId); } /// /// Forwards the picker's selection to the active handler, honouring the live-vs-on-close policy. /// private void HandlePickerCommands() { Event evt = Event.current; if (evt == null || evt.type != EventType.ExecuteCommand) return; if (EditorGUIUtility.GetObjectPickerControlID() != RecipePickerControlId) return; bool updated = evt.commandName == "ObjectSelectorUpdated"; bool closed = evt.commandName == "ObjectSelectorClosed"; if (!updated && !closed) return; if (updated && pickCommitsOnCloseOnly) return; pickHandler?.Invoke(EditorGUIUtility.GetObjectPickerObject() as ItemData); } #endregion } }