using System; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using Ashwild.Building; namespace Ashwild.EditorTools { /// /// The custom editor for a BuildableData shown in the right pane of the Ashwild Database. Replaces /// the default ScriptableObject inspector with a hero header (large clickable icon, editable name, /// "Buildable" badge) and cards for the identity (description) and the ghost/built prefabs the /// construction menu and placement ghost consume. All fields bind live to the asset; changing the /// name or icon notifies the list so the row updates without a full rebuild. /// public class BuildableEditorView { #region State public VisualElement Root { get; } private const int IconPickerControlId = 0x41534842; private readonly BuildableData buildable; private readonly SerializedObject so; private readonly Action onMetaChanged; private VisualElement iconPreview; #endregion #region Construction /// /// Builds the full editor tree for a buildable. is raised when /// the display name or icon changes so the list can refresh that row. /// public BuildableEditorView(BuildableData buildable, Action onMetaChanged) { this.buildable = buildable; this.onMetaChanged = onMetaChanged; so = new SerializedObject(buildable); Root = new VisualElement(); Root.Add(BuildHero()); Root.Add(BuildIdentityCard()); Root.Add(BuildPrefabsCard()); } #endregion #region Hero /// /// Builds the hero header: a large clickable icon preview, the editable display name and the /// "Buildable" badge. Name/icon changes notify the list so its row follows immediately. /// 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 buildable icon"; iconPreview.RegisterCallback(_ => OpenIconPicker()); UpdateIconPreview(buildable.Icon); hero.Add(iconPreview); IMGUIContainer 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 = buildable.DisplayName }; nameField.AddToClassList("ash-hero__name"); nameField.BindProperty(so.FindProperty("displayName")); nameField.RegisterValueChangedCallback(_ => onMetaChanged?.Invoke()); info.Add(AshwildUI.EditableNameRow(nameField)); VisualElement typeRow = new VisualElement(); typeRow.AddToClassList("ash-hero__typerow"); typeRow.Add(AshwildUI.Badge("Buildable", AshwildUI.BuildableColor)); info.Add(typeRow); hero.Add(info); return hero; } /// /// Shows the buildable's icon in the hero preview, or a neutral placeholder when none is set. /// private void UpdateIconPreview(Sprite sprite) { iconPreview.style.backgroundImage = sprite != null ? new StyleBackground(sprite) : new StyleBackground(); } /// /// Opens Unity's sprite object picker, seeded with the current icon, so the designer can change /// the buildable icon by clicking the hero preview directly. /// private void OpenIconPicker() { EditorGUIUtility.ShowObjectPicker(buildable.Icon, false, string.Empty, IconPickerControlId); } /// /// Listens (through the hidden IMGUI proxy) for the picker selecting/closing on our control id /// and applies the chosen sprite live, so the hero preview and the list row update immediately. /// 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); } /// /// Writes the picked icon onto the asset (no-op when unchanged) and refreshes the hero preview /// and the list row. /// 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 /// /// Identity card: the description shown on the menu card, plus the icon field (kept in sync /// with the hero preview and the list row when edited here). /// 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; } /// /// Prefabs card: the semi-transparent ghost spawned while positioning and the real structure /// spawned once placement is confirmed. /// private VisualElement BuildPrefabsCard() { VisualElement card = AshwildUI.Card("Prefabs"); ObjectField ghost = new ObjectField("Ghost Prefab") { objectType = typeof(GameObject), allowSceneObjects = false }; ghost.BindProperty(so.FindProperty("ghostPrefab")); card.Add(ghost); ObjectField built = new ObjectField("Built Prefab") { objectType = typeof(GameObject), allowSceneObjects = false }; built.BindProperty(so.FindProperty("builtPrefab")); card.Add(built); return card; } #endregion } }