(Feat) Tools Creation
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
using System;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using Ashwild.Inventory;
|
||||
|
||||
namespace Ashwild.EditorTools
|
||||
{
|
||||
/// <summary>
|
||||
/// The custom, context-aware editor for an ItemData shown in the right pane of the Ashwild
|
||||
/// Database. Replaces the default ScriptableObject inspector with a hero header (large icon, name,
|
||||
/// type badge) and a set of cards that reveal themselves only when relevant — the Tool card for
|
||||
/// tools/weapons, the Consumable card for consumables, fuel duration only when the item is fuel.
|
||||
/// All fields bind live to the asset, and the bottom card generates the world/hand prefabs by
|
||||
/// wrapping a chosen source object (see ItemAssetFactory).
|
||||
/// </summary>
|
||||
public class ItemEditorView
|
||||
{
|
||||
#region State
|
||||
|
||||
public VisualElement Root { get; }
|
||||
|
||||
private readonly ItemData item;
|
||||
private readonly SerializedObject so;
|
||||
private readonly Action onMetaChanged;
|
||||
private readonly Action onPrefabGenerated;
|
||||
|
||||
private const int IconPickerControlId = 0x41534849;
|
||||
|
||||
private VisualElement iconPreview;
|
||||
private VisualElement badgeHolder;
|
||||
private VisualElement toolCard;
|
||||
private VisualElement consumableCard;
|
||||
private VisualElement cookingCard;
|
||||
private VisualElement fuelCard;
|
||||
private VisualElement maxStackRow;
|
||||
private VisualElement fuelSecondsRow;
|
||||
private VisualElement generationCard;
|
||||
private Button worldGenButton;
|
||||
private Button handGenButton;
|
||||
private ObjectField worldField;
|
||||
private ObjectField handField;
|
||||
private IMGUIContainer iconPickerProxy;
|
||||
private ItemType currentType;
|
||||
private GameObject pendingSource;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Construction
|
||||
|
||||
/// <summary>
|
||||
/// Builds the full editor tree for an item. <paramref name="onMetaChanged"/> is raised when the
|
||||
/// name, icon or type changes so the list can refresh that row; <paramref name="onPrefabGenerated"/>
|
||||
/// after a prefab is built so the view re-renders with the new reference.
|
||||
/// </summary>
|
||||
public ItemEditorView(ItemData item, Action onMetaChanged, Action onPrefabGenerated)
|
||||
{
|
||||
this.item = item;
|
||||
this.onMetaChanged = onMetaChanged;
|
||||
this.onPrefabGenerated = onPrefabGenerated;
|
||||
so = new SerializedObject(item);
|
||||
|
||||
Root = new VisualElement();
|
||||
Root.Add(BuildHero());
|
||||
Root.Add(BuildIdentityCard());
|
||||
Root.Add(BuildStackingCard());
|
||||
Root.Add(BuildToolCard());
|
||||
Root.Add(BuildConsumableCard());
|
||||
Root.Add(BuildCookingCard());
|
||||
Root.Add(BuildFuelCard());
|
||||
Root.Add(BuildPrefabsCard());
|
||||
Root.Add(BuildGenerationCard());
|
||||
|
||||
currentType = item.ItemType;
|
||||
ApplyTypeVisibility(currentType);
|
||||
SetRowVisible(maxStackRow, item.IsStackable);
|
||||
SetRowVisible(fuelSecondsRow, item.IsFuel);
|
||||
UpdateGenerationVisibility();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Hero
|
||||
|
||||
/// <summary>
|
||||
/// Builds the hero header: a large icon preview, the editable item name, the type dropdown and
|
||||
/// the coloured type badge. Name/type changes notify the list; the badge and conditional cards
|
||||
/// react to the type immediately.
|
||||
/// </summary>
|
||||
private VisualElement BuildHero()
|
||||
{
|
||||
VisualElement hero = new VisualElement();
|
||||
hero.AddToClassList("ash-hero");
|
||||
|
||||
iconPreview = new VisualElement();
|
||||
iconPreview.AddToClassList("ash-hero__icon");
|
||||
iconPreview.AddToClassList("ash-hero__icon--clickable");
|
||||
iconPreview.tooltip = "Click to change the item icon";
|
||||
iconPreview.RegisterCallback<ClickEvent>(_ => OpenIconPicker());
|
||||
UpdateIconPreview(item.Icon);
|
||||
hero.Add(iconPreview);
|
||||
|
||||
iconPickerProxy = new IMGUIContainer(HandleIconPickerCommands);
|
||||
iconPickerProxy.style.position = Position.Absolute;
|
||||
iconPickerProxy.style.width = 1;
|
||||
iconPickerProxy.style.height = 1;
|
||||
hero.Add(iconPickerProxy);
|
||||
|
||||
VisualElement info = new VisualElement();
|
||||
info.AddToClassList("ash-hero__info");
|
||||
|
||||
TextField nameField = new TextField { value = item.ItemName };
|
||||
nameField.AddToClassList("ash-hero__name");
|
||||
nameField.BindProperty(so.FindProperty("itemName"));
|
||||
nameField.RegisterValueChangedCallback(_ => onMetaChanged?.Invoke());
|
||||
info.Add(nameField);
|
||||
|
||||
VisualElement typeRow = new VisualElement();
|
||||
typeRow.AddToClassList("ash-hero__typerow");
|
||||
|
||||
EnumField typeField = new EnumField(item.ItemType);
|
||||
typeField.AddToClassList("ash-hero__type");
|
||||
typeField.BindProperty(so.FindProperty("itemType"));
|
||||
typeField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
currentType = (ItemType)evt.newValue;
|
||||
ApplyTypeVisibility(currentType);
|
||||
RebuildBadge(currentType);
|
||||
UpdateGenerationVisibility();
|
||||
onMetaChanged?.Invoke();
|
||||
});
|
||||
typeRow.Add(typeField);
|
||||
|
||||
badgeHolder = new VisualElement();
|
||||
badgeHolder.AddToClassList("ash-hero__badge");
|
||||
RebuildBadge(item.ItemType);
|
||||
typeRow.Add(badgeHolder);
|
||||
|
||||
info.Add(typeRow);
|
||||
hero.Add(info);
|
||||
|
||||
return hero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paints the type badge for the given type, replacing any previous one.
|
||||
/// </summary>
|
||||
private void RebuildBadge(ItemType type)
|
||||
{
|
||||
badgeHolder.Clear();
|
||||
badgeHolder.Add(AshwildUI.Badge(type.ToString(), AshwildUI.TypeColor(type)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the item's icon in the hero preview, or a neutral placeholder when none is set.
|
||||
/// </summary>
|
||||
private void UpdateIconPreview(Sprite sprite)
|
||||
{
|
||||
iconPreview.style.backgroundImage = sprite != null ? new StyleBackground(sprite) : new StyleBackground();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens Unity's sprite object picker, seeded with the current icon, so the designer can change
|
||||
/// the item icon by clicking the hero preview directly.
|
||||
/// </summary>
|
||||
private void OpenIconPicker()
|
||||
{
|
||||
EditorGUIUtility.ShowObjectPicker<Sprite>(item.Icon, false, string.Empty, IconPickerControlId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Listens (through the hidden IMGUI proxy) for the picker selecting/closing on our control id
|
||||
/// and applies the chosen sprite live. ObjectSelectorUpdated fires on each highlight, so the
|
||||
/// hero, the bound Icon field and the list all preview the change immediately.
|
||||
/// </summary>
|
||||
private void HandleIconPickerCommands()
|
||||
{
|
||||
Event evt = Event.current;
|
||||
if (evt == null || evt.type != EventType.ExecuteCommand) return;
|
||||
if (EditorGUIUtility.GetObjectPickerControlID() != IconPickerControlId) return;
|
||||
if (evt.commandName != "ObjectSelectorUpdated" && evt.commandName != "ObjectSelectorClosed") return;
|
||||
|
||||
AssignIcon(EditorGUIUtility.GetObjectPickerObject() as Sprite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the picked icon onto the asset (no-op when unchanged) and refreshes the hero preview
|
||||
/// and the list row.
|
||||
/// </summary>
|
||||
private void AssignIcon(Sprite sprite)
|
||||
{
|
||||
SerializedProperty iconProp = so.FindProperty("icon");
|
||||
if (iconProp.objectReferenceValue == sprite) return;
|
||||
|
||||
iconProp.objectReferenceValue = sprite;
|
||||
so.ApplyModifiedProperties();
|
||||
UpdateIconPreview(sprite);
|
||||
onMetaChanged?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cards
|
||||
|
||||
/// <summary>
|
||||
/// Identity card: description and icon. Editing the icon updates the hero preview and the list.
|
||||
/// </summary>
|
||||
private VisualElement BuildIdentityCard()
|
||||
{
|
||||
VisualElement card = AshwildUI.Card("Identity");
|
||||
|
||||
TextField description = new TextField("Description") { multiline = true };
|
||||
description.AddToClassList("ash-description");
|
||||
description.BindProperty(so.FindProperty("description"));
|
||||
card.Add(description);
|
||||
|
||||
ObjectField icon = new ObjectField("Icon") { objectType = typeof(Sprite), allowSceneObjects = false };
|
||||
icon.BindProperty(so.FindProperty("icon"));
|
||||
icon.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
UpdateIconPreview(evt.newValue as Sprite);
|
||||
onMetaChanged?.Invoke();
|
||||
});
|
||||
card.Add(icon);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stacking card: the stackable toggle and a max-stack field that only shows while stackable.
|
||||
/// </summary>
|
||||
private VisualElement BuildStackingCard()
|
||||
{
|
||||
VisualElement card = AshwildUI.Card("Stacking");
|
||||
|
||||
Toggle stackable = new Toggle("Is Stackable");
|
||||
stackable.BindProperty(so.FindProperty("isStackable"));
|
||||
stackable.RegisterValueChangedCallback(evt => SetRowVisible(maxStackRow, evt.newValue));
|
||||
card.Add(stackable);
|
||||
|
||||
maxStackRow = new IntegerField("Max Stack Size");
|
||||
((IntegerField)maxStackRow).BindProperty(so.FindProperty("maxStackSize"));
|
||||
card.Add(maxStackRow);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tool card: harvest type and tool power. Only shown for tools and weapons.
|
||||
/// </summary>
|
||||
private VisualElement BuildToolCard()
|
||||
{
|
||||
toolCard = AshwildUI.Card("Tool");
|
||||
|
||||
EnumField harvest = new EnumField("Harvest Type", item.HarvestType);
|
||||
harvest.BindProperty(so.FindProperty("harvestType"));
|
||||
toolCard.Add(harvest);
|
||||
|
||||
FloatField power = new FloatField("Tool Power");
|
||||
power.BindProperty(so.FindProperty("toolPower"));
|
||||
toolCard.Add(power);
|
||||
|
||||
return toolCard;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consumable card: the restore values. Only shown for consumables.
|
||||
/// </summary>
|
||||
private VisualElement BuildConsumableCard()
|
||||
{
|
||||
consumableCard = AshwildUI.Card("Consumable");
|
||||
consumableCard.Add(BoundFloat("Health Restore", "healthRestore"));
|
||||
consumableCard.Add(BoundFloat("Hunger Restore", "hungerRestore"));
|
||||
consumableCard.Add(BoundFloat("Thirst Restore", "thirstRestore"));
|
||||
return consumableCard;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cooking card: the cooked result item and its cook time (applies to any item that can cook).
|
||||
/// </summary>
|
||||
private VisualElement BuildCookingCard()
|
||||
{
|
||||
cookingCard = AshwildUI.Card("Cooking");
|
||||
|
||||
ObjectField cooked = new ObjectField("Cooked Result") { objectType = typeof(ItemData), allowSceneObjects = false };
|
||||
cooked.BindProperty(so.FindProperty("cookedResult"));
|
||||
cookingCard.Add(cooked);
|
||||
|
||||
cookingCard.Add(BoundFloat("Cook Time", "cookTime"));
|
||||
return cookingCard;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fuel card: the fuel toggle and a burn-duration field revealed only while the item is fuel.
|
||||
/// </summary>
|
||||
private VisualElement BuildFuelCard()
|
||||
{
|
||||
fuelCard = AshwildUI.Card("Fuel");
|
||||
|
||||
Toggle isFuel = new Toggle("Is Fuel");
|
||||
isFuel.BindProperty(so.FindProperty("isFuel"));
|
||||
isFuel.RegisterValueChangedCallback(evt => SetRowVisible(fuelSecondsRow, evt.newValue));
|
||||
fuelCard.Add(isFuel);
|
||||
|
||||
fuelSecondsRow = BoundFloat("Fuel Seconds", "fuelSeconds");
|
||||
fuelCard.Add(fuelSecondsRow);
|
||||
|
||||
return fuelCard;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prefabs card: the world and hand prefab references (auto-filled by generation, editable here).
|
||||
/// </summary>
|
||||
private VisualElement BuildPrefabsCard()
|
||||
{
|
||||
VisualElement card = AshwildUI.Card("Prefabs");
|
||||
|
||||
worldField = new ObjectField("World Prefab") { objectType = typeof(GameObject), allowSceneObjects = false };
|
||||
worldField.BindProperty(so.FindProperty("worldPrefab"));
|
||||
worldField.RegisterValueChangedCallback(_ => UpdateGenerationVisibility());
|
||||
card.Add(worldField);
|
||||
|
||||
handField = new ObjectField("Hand Prefab") { objectType = typeof(GameObject), allowSceneObjects = false };
|
||||
handField.BindProperty(so.FindProperty("handPrefab"));
|
||||
handField.RegisterValueChangedCallback(_ => UpdateGenerationVisibility());
|
||||
card.Add(handField);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generation card: pick a source model/prefab, then build the world pickup (always) and the
|
||||
/// in-hand prefab (tools/weapons only) wrapped around it.
|
||||
/// </summary>
|
||||
private VisualElement BuildGenerationCard()
|
||||
{
|
||||
generationCard = AshwildUI.Card("Prefab Generation");
|
||||
generationCard.AddToClassList("ash-card--accent");
|
||||
|
||||
ObjectField source = new ObjectField("Source (Prefab / Model)") { objectType = typeof(GameObject), allowSceneObjects = false };
|
||||
source.RegisterValueChangedCallback(evt => pendingSource = evt.newValue as GameObject);
|
||||
generationCard.Add(source);
|
||||
|
||||
VisualElement buttons = new VisualElement();
|
||||
buttons.AddToClassList("ash-buttons");
|
||||
|
||||
worldGenButton = new Button(GenerateWorld) { text = "Generate World Prefab" };
|
||||
worldGenButton.AddToClassList("ash-btn");
|
||||
worldGenButton.AddToClassList("ash-btn--primary");
|
||||
buttons.Add(worldGenButton);
|
||||
|
||||
handGenButton = new Button(GenerateHand) { text = "Generate Hand Prefab" };
|
||||
handGenButton.AddToClassList("ash-btn");
|
||||
buttons.Add(handGenButton);
|
||||
|
||||
generationCard.Add(buttons);
|
||||
return generationCard;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Actions
|
||||
|
||||
/// <summary>
|
||||
/// Builds the world pickup prefab around the chosen source and re-renders on success.
|
||||
/// </summary>
|
||||
private void GenerateWorld()
|
||||
{
|
||||
if (ItemAssetFactory.GenerateWorldPrefab(item, pendingSource) != null)
|
||||
onPrefabGenerated?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the in-hand prefab around the chosen source and re-renders on success.
|
||||
/// </summary>
|
||||
private void GenerateHand()
|
||||
{
|
||||
if (ItemAssetFactory.GenerateHandPrefab(item, pendingSource) != null)
|
||||
onPrefabGenerated?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Conditional Visibility
|
||||
|
||||
/// <summary>
|
||||
/// Reveals only the cards that make sense for the item type: Tool for tools/weapons; Consumable
|
||||
/// for consumables; Cooking for consumables (food) and materials (smelting ore → ingot), but
|
||||
/// never for a tool or weapon; and Fuel for materials only (a consumable is never burned).
|
||||
/// </summary>
|
||||
private void ApplyTypeVisibility(ItemType type)
|
||||
{
|
||||
SetRowVisible(toolCard, type == ItemType.Tool || type == ItemType.Weapon);
|
||||
SetRowVisible(consumableCard, type == ItemType.Consumable);
|
||||
SetRowVisible(cookingCard, type == ItemType.Consumable || type == ItemType.Material);
|
||||
SetRowVisible(fuelCard, type == ItemType.Material);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides a generation button once its prefab already exists (and the hand button entirely for
|
||||
/// non-tools), and collapses the whole generation card when nothing is left to generate — so a
|
||||
/// fully wired item shows no redundant tooling. Re-evaluated when the type or a prefab field changes.
|
||||
/// </summary>
|
||||
private void UpdateGenerationVisibility()
|
||||
{
|
||||
if (worldField == null || handField == null || generationCard == null) return;
|
||||
|
||||
bool isToolLike = currentType == ItemType.Tool || currentType == ItemType.Weapon;
|
||||
bool showWorld = worldField.value == null;
|
||||
bool showHand = isToolLike && handField.value == null;
|
||||
|
||||
SetRowVisible(worldGenButton, showWorld);
|
||||
SetRowVisible(handGenButton, showHand);
|
||||
SetRowVisible(generationCard, showWorld || showHand);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collapses or shows an element without leaving a gap when hidden.
|
||||
/// </summary>
|
||||
private static void SetRowVisible(VisualElement element, bool visible)
|
||||
{
|
||||
if (element != null) element.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds a float field already bound to a serialized property.
|
||||
/// </summary>
|
||||
private FloatField BoundFloat(string label, string property)
|
||||
{
|
||||
FloatField field = new FloatField(label);
|
||||
field.BindProperty(so.FindProperty(property));
|
||||
return field;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user