Merge remote-tracking branch 'origin/feat/inport-props' into feat/craft-discovery
# Conflicts: # Assets/External/Animated PBR Chest Demo/Materials/WoodChest.mat # Packages/com.distantlands.cozy.core/Content/Integration/Import for BiRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for HDRP.unitypackage.meta # Packages/com.distantlands.cozy.core/Content/Integration/Import for URP.unitypackage.meta
This commit is contained in:
@@ -59,11 +59,6 @@
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.ash-search {
|
||||
width: 180px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* ── Body split ──────────────────────────────────────────── */
|
||||
.ash-body {
|
||||
flex-direction: row;
|
||||
@@ -81,6 +76,57 @@
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* ── Left-pane filter header (search + type) ─────────────── */
|
||||
.ash-listheader {
|
||||
padding-top: 6px;
|
||||
padding-bottom: 6px;
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-color: rgb(18, 19, 22);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ash-listsearch {
|
||||
margin: 0;
|
||||
width: auto;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
background-color: rgb(46, 47, 52);
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.ash-listsearch .unity-toolbar-search-field__search-button {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.ash-listsearch .unity-base-text-field__input {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.ash-typefilter {
|
||||
margin-top: 6px;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ash-typefilter .unity-base-field__label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ash-typefilter .unity-base-popup-field__input {
|
||||
border-radius: 4px;
|
||||
background-color: rgb(46, 47, 52);
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.ash-right {
|
||||
flex-grow: 1;
|
||||
padding: 12px;
|
||||
@@ -277,6 +323,15 @@
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Small italic note clarifying a field inside a card */
|
||||
.ash-card__hint {
|
||||
margin-top: 6px;
|
||||
font-size: 10px;
|
||||
color: rgb(130, 132, 140);
|
||||
-unity-font-style: italic;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.ash-description .unity-text-field__input {
|
||||
min-height: 48px;
|
||||
white-space: normal;
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace Ashwild.EditorTools
|
||||
#region Constants
|
||||
|
||||
private const string UssPath = "Assets/GAME/Script/Editor/Database/AshwildDatabase.uss";
|
||||
private const string AllTypesLabel = "All Types";
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -38,6 +39,7 @@ namespace Ashwild.EditorTools
|
||||
|
||||
private Tab activeTab = Tab.Items;
|
||||
private string searchFilter = string.Empty;
|
||||
private string itemTypeFilter = AllTypesLabel;
|
||||
|
||||
private readonly List<Object> sourceItems = new List<Object>();
|
||||
private Object selectedAsset;
|
||||
@@ -51,6 +53,7 @@ namespace Ashwild.EditorTools
|
||||
private Button itemsTab;
|
||||
private Button recipesTab;
|
||||
private Button buildablesTab;
|
||||
private PopupField<string> typeFilterField;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -106,7 +109,8 @@ namespace Ashwild.EditorTools
|
||||
#region Toolbar
|
||||
|
||||
/// <summary>
|
||||
/// Builds the top toolbar: the two family tabs, a search field, and the database rebuild action.
|
||||
/// Builds the top toolbar: the three family tabs and the database rebuild action. The search
|
||||
/// field and type filter live in the left-pane header, directly above the list they filter.
|
||||
/// </summary>
|
||||
private VisualElement BuildToolbar()
|
||||
{
|
||||
@@ -124,15 +128,6 @@ namespace Ashwild.EditorTools
|
||||
spacer.AddToClassList("ash-toolbar-spacer");
|
||||
toolbar.Add(spacer);
|
||||
|
||||
ToolbarSearchField search = new ToolbarSearchField();
|
||||
search.AddToClassList("ash-search");
|
||||
search.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
searchFilter = evt.newValue ?? string.Empty;
|
||||
RefreshList();
|
||||
});
|
||||
toolbar.Add(search);
|
||||
|
||||
Button rebuild = new Button(ItemDatabaseBuilder.Rebuild) { text = "Rebuild DB" };
|
||||
rebuild.AddToClassList("ash-btn");
|
||||
rebuild.tooltip = "Re-scan every ItemData and rewrite the network ItemDatabase.";
|
||||
@@ -175,6 +170,8 @@ namespace Ashwild.EditorTools
|
||||
recipesTab.EnableInClassList("ash-tab--active", tab == Tab.Recipes);
|
||||
buildablesTab.EnableInClassList("ash-tab--active", tab == Tab.Buildables);
|
||||
|
||||
UpdateTypeFilterVisibility();
|
||||
|
||||
selectedAsset = null;
|
||||
RefreshList();
|
||||
ShowSelection();
|
||||
@@ -185,13 +182,16 @@ namespace Ashwild.EditorTools
|
||||
#region Left Pane
|
||||
|
||||
/// <summary>
|
||||
/// Builds the left pane: a styled list of the active family plus a footer "New" button.
|
||||
/// Builds the left pane: a filter header (search + item-type filter), a styled list of the
|
||||
/// active family, and a footer "New" button.
|
||||
/// </summary>
|
||||
private VisualElement BuildLeftPane()
|
||||
{
|
||||
VisualElement left = new VisualElement();
|
||||
left.AddToClassList("ash-left");
|
||||
|
||||
left.Add(BuildListHeader());
|
||||
|
||||
listView = new ListView(sourceItems)
|
||||
{
|
||||
fixedItemHeight = 48,
|
||||
@@ -214,6 +214,58 @@ namespace Ashwild.EditorTools
|
||||
return left;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the header sitting above the list: a search field that filters by name across every
|
||||
/// family, and an item-type dropdown that narrows the item list to a single type. The type
|
||||
/// filter is only meaningful for items, so it is hidden on the Recipes and Buildables tabs.
|
||||
/// </summary>
|
||||
private VisualElement BuildListHeader()
|
||||
{
|
||||
VisualElement header = new VisualElement();
|
||||
header.AddToClassList("ash-listheader");
|
||||
|
||||
ToolbarSearchField search = new ToolbarSearchField();
|
||||
search.AddToClassList("ash-listsearch");
|
||||
search.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
searchFilter = evt.newValue ?? string.Empty;
|
||||
RefreshList();
|
||||
});
|
||||
header.Add(search);
|
||||
|
||||
List<string> typeChoices = new List<string> { AllTypesLabel };
|
||||
typeChoices.AddRange(System.Enum.GetNames(typeof(ItemType)));
|
||||
|
||||
typeFilterField = new PopupField<string>(typeChoices, 0);
|
||||
typeFilterField.AddToClassList("ash-typefilter");
|
||||
typeFilterField.tooltip = "Show only items of the selected type.";
|
||||
typeFilterField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
itemTypeFilter = evt.newValue ?? AllTypesLabel;
|
||||
RefreshList();
|
||||
});
|
||||
header.Add(typeFilterField);
|
||||
|
||||
return header;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the item-type dropdown only on the Items tab and resets it to "all" when leaving, so a
|
||||
/// stale type filter never silently narrows a list it cannot apply to.
|
||||
/// </summary>
|
||||
private void UpdateTypeFilterVisibility()
|
||||
{
|
||||
if (typeFilterField == null) return;
|
||||
|
||||
bool showForItems = activeTab == Tab.Items;
|
||||
typeFilterField.style.display = showForItems ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
if (!showForItems)
|
||||
{
|
||||
itemTypeFilter = AllTypesLabel;
|
||||
typeFilterField.SetValueWithoutNotify(AllTypesLabel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the reusable visual for a single list row (icon, name, type tag).
|
||||
/// </summary>
|
||||
@@ -251,6 +303,7 @@ namespace Ashwild.EditorTools
|
||||
{
|
||||
ItemData item => AshwildUI.TypeColor(item.ItemType),
|
||||
BuildableData => AshwildUI.BuildableColor,
|
||||
CraftingRecipe recipe when recipe.RequiredStation != CraftingStationType.None => AshwildUI.StationColor,
|
||||
_ => new Color(0.5f, 0.52f, 0.58f)
|
||||
};
|
||||
|
||||
@@ -421,6 +474,9 @@ namespace Ashwild.EditorTools
|
||||
if (!string.IsNullOrEmpty(searchFilter)
|
||||
&& AssetDisplayName(asset).IndexOf(searchFilter, System.StringComparison.OrdinalIgnoreCase) < 0)
|
||||
continue;
|
||||
if (activeTab == Tab.Items && itemTypeFilter != AllTypesLabel
|
||||
&& asset is ItemData item && item.ItemType.ToString() != itemTypeFilter)
|
||||
continue;
|
||||
sourceItems.Add(asset);
|
||||
}
|
||||
|
||||
@@ -465,12 +521,15 @@ namespace Ashwild.EditorTools
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The short type tag shown on the right of a list row (item type, or "Recipe").
|
||||
/// The short type tag shown on the right of a list row. A recipe reports the station it is
|
||||
/// crafted on rather than a flat "Recipe", so scanning the list tells which recipes are bench-
|
||||
/// locked; hand recipes (station None) keep reading "Recipe".
|
||||
/// </summary>
|
||||
private static string AssetTag(Object asset)
|
||||
{
|
||||
if (asset is ItemData item) return item.ItemType.ToString();
|
||||
if (asset is CraftingRecipe) return "Recipe";
|
||||
if (asset is CraftingRecipe recipe)
|
||||
return recipe.RequiredStation == CraftingStationType.None ? "Recipe" : recipe.RequiredStation.ToString();
|
||||
if (asset is BuildableData) return "Buildable";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ namespace Ashwild.EditorTools
|
||||
/// </summary>
|
||||
public static readonly Color BuildableColor = new Color(0.38f, 0.64f, 0.86f);
|
||||
|
||||
/// <summary>
|
||||
/// The accent colour marking a recipe that is locked to a workstation, so the bench-only ones
|
||||
/// stand out from the plain (hand-craftable) rows at a glance.
|
||||
/// </summary>
|
||||
public static readonly Color StationColor = new Color(0.76f, 0.60f, 0.90f);
|
||||
|
||||
/// <summary>
|
||||
/// The accent colour that identifies an item type across the list badges, hero and cards.
|
||||
/// </summary>
|
||||
|
||||
@@ -10,11 +10,13 @@ namespace Ashwild.EditorTools
|
||||
/// <summary>
|
||||
/// Editor-only factory that creates ItemData assets and, on demand, builds the world pickup
|
||||
/// prefab (Pickable + collider on the Pickable layer) and the in-hand prefab (ToolBehaviour +
|
||||
/// HeldItemOffset on the Tools layer) by wrapping a chosen source object — a model (FBX) or an
|
||||
/// existing prefab. The source is nested as a child so the designer's authored model stays intact
|
||||
/// and editable, while the generated root carries the gameplay components and a collider auto-fit
|
||||
/// to the source's renderers. Every reference is wired back onto the ItemData. Follows §3 of the
|
||||
/// project rules: world pickups are plain WorldObjects (registry-synced), never NetworkObjects.
|
||||
/// HeldItemOffset on the Tools layer) as a Prefab Variant of a chosen source prefab. The designer
|
||||
/// authors one base prefab with its meshes and materials set up (the raw FBX comes in untextured),
|
||||
/// hands it to the generator, and each generated prefab becomes a variant that carries the gameplay
|
||||
/// components as overrides on its own root — so re-texturing the base propagates to both variants.
|
||||
/// A non-prefab source falls back to a plain duplicate. Every reference is wired back onto the
|
||||
/// ItemData. Follows §3 of the project rules: world pickups are plain WorldObjects (registry-synced),
|
||||
/// never NetworkObjects.
|
||||
/// </summary>
|
||||
public static class ItemAssetFactory
|
||||
{
|
||||
@@ -24,6 +26,9 @@ namespace Ashwild.EditorTools
|
||||
private const string WorldPrefabFolder = "Assets/GAME/Prefabs/Pickable";
|
||||
private const string HandPrefabFolder = "Assets/GAME/Prefabs/Tools";
|
||||
|
||||
private const string WorldPrefabSuffix = "_Pickable";
|
||||
private const string HandPrefabSuffix = "_Tools";
|
||||
|
||||
private const string PickableLayerName = "Pickable";
|
||||
private const string ToolsLayerName = "Tools";
|
||||
private const string HarvestableLayerName = "Harvestable";
|
||||
@@ -65,19 +70,20 @@ namespace Ashwild.EditorTools
|
||||
#region World Prefab
|
||||
|
||||
/// <summary>
|
||||
/// Builds the world pickup prefab for an item by wrapping the source object: a root on the
|
||||
/// Pickable layer carries the source as a child, an auto-fitted non-trigger BoxCollider (so the
|
||||
/// interactor ray — which reads the Pickable off the hit collider's own GameObject — has a
|
||||
/// target on the root), and the Pickable component wired to this item. The id stays -1; scene
|
||||
/// instances get a baked id later via Tools ▸ Ashwild ▸ Assign World Object IDs. The finished
|
||||
/// prefab is assigned back onto ItemData.worldPrefab. Returns the saved prefab, or null on error.
|
||||
/// Builds the world pickup prefab for an item as a variant of the source prefab: the variant root
|
||||
/// sits on the Pickable layer, gains an auto-fitted non-trigger BoxCollider (so the interactor ray
|
||||
/// — which reads the Pickable off the hit collider's own GameObject — has a target on the root),
|
||||
/// and a Pickable wired to this item. The id stays -1; scene instances get a baked id later via
|
||||
/// Tools ▸ Ashwild ▸ Setup World Object IDs. The finished prefab is assigned back onto
|
||||
/// ItemData.worldPrefab. Returns the saved prefab, or null on error.
|
||||
/// </summary>
|
||||
public static GameObject GenerateWorldPrefab(ItemData item, GameObject source)
|
||||
{
|
||||
if (!ValidateInputs(item, source, "world")) return null;
|
||||
if (!EnsureFolder(WorldPrefabFolder)) return null;
|
||||
|
||||
GameObject root = BuildRoot(item, source, ResolveLayer(PickableLayerName));
|
||||
string prefabName = item.name + WorldPrefabSuffix;
|
||||
GameObject root = InstantiateSourceRoot(source, ResolveLayer(PickableLayerName), prefabName);
|
||||
FitBoxCollider(root);
|
||||
|
||||
Pickable pickable = root.AddComponent<Pickable>();
|
||||
@@ -85,7 +91,7 @@ namespace Ashwild.EditorTools
|
||||
so.FindProperty("itemData").objectReferenceValue = item;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
return SaveAndAssign(root, WorldPrefabFolder, item, "worldPrefab");
|
||||
return SaveAndAssign(root, WorldPrefabFolder, item, "worldPrefab", prefabName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -93,18 +99,19 @@ namespace Ashwild.EditorTools
|
||||
#region Hand Prefab
|
||||
|
||||
/// <summary>
|
||||
/// Builds the in-hand prefab for a tool/weapon by wrapping the source object: a root on the
|
||||
/// Tools layer carries the source as a child, a ToolBehaviour (harvest ray masked to the
|
||||
/// Harvestable layer) and a HeldItemOffset so it can be posed in the holder. No collider —
|
||||
/// held items carry no physics. The prefab is assigned back onto ItemData.handPrefab.
|
||||
/// Returns the prefab, or null on error.
|
||||
/// Builds the in-hand prefab for a tool/weapon as a variant of the source prefab: the variant
|
||||
/// root sits on the Tools layer and gains a ToolBehaviour (harvest ray masked to the Harvestable
|
||||
/// layer) and a HeldItemOffset so it can be posed in the holder. No collider — held items carry
|
||||
/// no physics. The prefab is assigned back onto ItemData.handPrefab. Returns the prefab, or null
|
||||
/// on error.
|
||||
/// </summary>
|
||||
public static GameObject GenerateHandPrefab(ItemData item, GameObject source)
|
||||
{
|
||||
if (!ValidateInputs(item, source, "hand")) return null;
|
||||
if (!EnsureFolder(HandPrefabFolder)) return null;
|
||||
|
||||
GameObject root = BuildRoot(item, source, ResolveLayer(ToolsLayerName));
|
||||
string prefabName = item.name + HandPrefabSuffix;
|
||||
GameObject root = InstantiateSourceRoot(source, ResolveLayer(ToolsLayerName), prefabName);
|
||||
|
||||
ToolBehaviour tool = root.AddComponent<ToolBehaviour>();
|
||||
SerializedObject so = new SerializedObject(tool);
|
||||
@@ -113,7 +120,7 @@ namespace Ashwild.EditorTools
|
||||
|
||||
root.AddComponent<HeldItemOffset>();
|
||||
|
||||
return SaveAndAssign(root, HandPrefabFolder, item, "handPrefab");
|
||||
return SaveAndAssign(root, HandPrefabFolder, item, "handPrefab", prefabName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -139,18 +146,20 @@ namespace Ashwild.EditorTools
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the generated root named after the item, on the given layer, with the source object
|
||||
/// (model or prefab) nested as a child at the origin so its authored transform is preserved as
|
||||
/// a prefab link rather than flattened.
|
||||
/// Instantiates the source prefab itself as the generated root — keeping the prefab connection so
|
||||
/// SaveAndAssign turns it into a variant — then applies the target name and layer as variant
|
||||
/// overrides. A non-prefab source falls back to a plain (unlinked) copy, which SaveAndAssign then
|
||||
/// saves as a regular prefab rather than a variant.
|
||||
/// </summary>
|
||||
private static GameObject BuildRoot(ItemData item, GameObject source, int layer)
|
||||
private static GameObject InstantiateSourceRoot(GameObject source, int layer, string rootName)
|
||||
{
|
||||
GameObject root = new GameObject(item.name) { layer = layer };
|
||||
GameObject root = PrefabUtility.InstantiatePrefab(source) as GameObject;
|
||||
if (root == null) root = Object.Instantiate(source);
|
||||
|
||||
GameObject visual = (GameObject)PrefabUtility.InstantiatePrefab(source);
|
||||
if (visual == null) visual = Object.Instantiate(source);
|
||||
visual.transform.SetParent(root.transform, false);
|
||||
visual.transform.localPosition = Vector3.zero;
|
||||
root.name = rootName;
|
||||
root.layer = layer;
|
||||
root.transform.position = Vector3.zero;
|
||||
root.transform.rotation = Quaternion.identity;
|
||||
|
||||
return root;
|
||||
}
|
||||
@@ -179,12 +188,13 @@ namespace Ashwild.EditorTools
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a built GameObject as a prefab under a unique path, destroys the scene instance,
|
||||
/// wires the saved prefab onto the given ItemData field, and refreshes the asset database.
|
||||
/// Saves a built GameObject as a prefab under a unique path (named with the type-specific suffix
|
||||
/// so pickables and hand tools stay distinguishable), destroys the scene instance, wires the
|
||||
/// saved prefab onto the given ItemData field, and refreshes the asset database.
|
||||
/// </summary>
|
||||
private static GameObject SaveAndAssign(GameObject root, string folder, ItemData item, string itemField)
|
||||
private static GameObject SaveAndAssign(GameObject root, string folder, ItemData item, string itemField, string prefabName)
|
||||
{
|
||||
string prefabPath = AssetDatabase.GenerateUniqueAssetPath($"{folder}/{item.name}.prefab");
|
||||
string prefabPath = AssetDatabase.GenerateUniqueAssetPath($"{folder}/{prefabName}.prefab");
|
||||
GameObject prefab = PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
|
||||
Object.DestroyImmediate(root);
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using Ashwild.Inventory;
|
||||
using Ashwild.Player;
|
||||
|
||||
namespace Ashwild.EditorTools
|
||||
{
|
||||
@@ -37,8 +40,6 @@ namespace Ashwild.EditorTools
|
||||
private VisualElement maxStackRow;
|
||||
private VisualElement fuelSecondsRow;
|
||||
private VisualElement generationCard;
|
||||
private Button worldGenButton;
|
||||
private Button handGenButton;
|
||||
private ObjectField worldField;
|
||||
private ObjectField handField;
|
||||
private IMGUIContainer iconPickerProxy;
|
||||
@@ -70,6 +71,7 @@ namespace Ashwild.EditorTools
|
||||
Root.Add(BuildCookingCard());
|
||||
Root.Add(BuildFuelCard());
|
||||
Root.Add(BuildPrefabsCard());
|
||||
Root.Add(BuildAnimationCard());
|
||||
Root.Add(BuildGenerationCard());
|
||||
|
||||
currentType = item.ItemType;
|
||||
@@ -114,6 +116,7 @@ namespace Ashwild.EditorTools
|
||||
nameField.AddToClassList("ash-hero__name");
|
||||
nameField.BindProperty(so.FindProperty("itemName"));
|
||||
nameField.RegisterValueChangedCallback(_ => onMetaChanged?.Invoke());
|
||||
nameField.RegisterCallback<FocusOutEvent>(_ => RenameAssetToItemName());
|
||||
info.Add(AshwildUI.EditableNameRow(nameField));
|
||||
|
||||
VisualElement typeRow = new VisualElement();
|
||||
@@ -201,6 +204,50 @@ namespace Ashwild.EditorTools
|
||||
|
||||
#endregion
|
||||
|
||||
#region Asset Naming
|
||||
|
||||
/// <summary>
|
||||
/// Renames the underlying asset file to match the authored item name (spaces → underscores) so
|
||||
/// the ScriptableObject on disk reads like the item it holds instead of the generic "NewItem".
|
||||
/// Runs on focus-out, not per keystroke, so the file is renamed once the name is committed; a
|
||||
/// blank name, an unchanged name, or a rename collision is skipped (the latter logged).
|
||||
/// </summary>
|
||||
private void RenameAssetToItemName()
|
||||
{
|
||||
string path = AssetDatabase.GetAssetPath(item);
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
|
||||
string desired = ToAssetFileName(item.ItemName);
|
||||
if (string.IsNullOrEmpty(desired)) return;
|
||||
|
||||
if (string.Equals(Path.GetFileNameWithoutExtension(path), desired, StringComparison.Ordinal)) return;
|
||||
|
||||
string error = AssetDatabase.RenameAsset(path, desired);
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
{
|
||||
Debug.LogError($"[ItemEditorView] Could not rename asset to '{desired}': {error}", item);
|
||||
return;
|
||||
}
|
||||
|
||||
onMetaChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns an authored item name into a valid asset file name: trims, collapses every whitespace
|
||||
/// run to a single underscore, and drops characters the filesystem forbids in a file name.
|
||||
/// </summary>
|
||||
private static string ToAssetFileName(string itemName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(itemName)) return string.Empty;
|
||||
|
||||
string underscored = Regex.Replace(itemName.Trim(), @"\s+", "_");
|
||||
foreach (char invalid in Path.GetInvalidFileNameChars())
|
||||
underscored = underscored.Replace(invalid.ToString(), string.Empty);
|
||||
return underscored;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cards
|
||||
|
||||
/// <summary>
|
||||
@@ -330,29 +377,47 @@ namespace Ashwild.EditorTools
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// Animation card: the clip sets this item imposes on each rig while it is held. Both are
|
||||
/// optional — an item that leaves them empty simply keeps the bare-hand animations, which is the
|
||||
/// right answer for every material and most consumables.
|
||||
/// </summary>
|
||||
private VisualElement BuildAnimationCard()
|
||||
{
|
||||
VisualElement card = AshwildUI.Card("Animation");
|
||||
|
||||
ObjectField armsField = new ObjectField("Arms Set") { objectType = typeof(PlayerAnimationSet), allowSceneObjects = false };
|
||||
armsField.BindProperty(so.FindProperty("armsAnimationSet"));
|
||||
card.Add(armsField);
|
||||
|
||||
ObjectField bodyField = new ObjectField("Body Set") { objectType = typeof(PlayerAnimationSet), allowSceneObjects = false };
|
||||
bodyField.BindProperty(so.FindProperty("bodyAnimationSet"));
|
||||
card.Add(bodyField);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generation card: pick a source prefab, then build both the world pickup (always) and the
|
||||
/// in-hand prefab (tools/weapons only) from it with one button, creating only the ones still
|
||||
/// missing.
|
||||
/// </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 };
|
||||
ObjectField source = new ObjectField("Source Prefab") { objectType = typeof(GameObject), allowSceneObjects = false };
|
||||
source.tooltip = "The set-up prefab (meshes + materials). Generation saves a Prefab Variant of it with the gameplay components added.";
|
||||
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);
|
||||
Button generate = new Button(GeneratePrefabs) { text = "Generate Prefab" };
|
||||
generate.AddToClassList("ash-btn");
|
||||
generate.AddToClassList("ash-btn--primary");
|
||||
buttons.Add(generate);
|
||||
|
||||
generationCard.Add(buttons);
|
||||
return generationCard;
|
||||
@@ -363,21 +428,23 @@ namespace Ashwild.EditorTools
|
||||
#region Actions
|
||||
|
||||
/// <summary>
|
||||
/// Builds the world pickup prefab around the chosen source and re-renders on success.
|
||||
/// Builds every prefab still missing for the item from the chosen source: the world pickup when
|
||||
/// none is set, and the in-hand prefab when the item is a tool/weapon and none is set. Re-renders
|
||||
/// once if anything was generated so the view reflects the new references and hides the card when
|
||||
/// nothing is left to build.
|
||||
/// </summary>
|
||||
private void GenerateWorld()
|
||||
private void GeneratePrefabs()
|
||||
{
|
||||
if (ItemAssetFactory.GenerateWorldPrefab(item, pendingSource) != null)
|
||||
onPrefabGenerated?.Invoke();
|
||||
}
|
||||
bool generated = false;
|
||||
|
||||
/// <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();
|
||||
if (worldField.value == null)
|
||||
generated |= ItemAssetFactory.GenerateWorldPrefab(item, pendingSource) != null;
|
||||
|
||||
bool isToolLike = currentType == ItemType.Tool || currentType == ItemType.Weapon;
|
||||
if (isToolLike && handField.value == null)
|
||||
generated |= ItemAssetFactory.GenerateHandPrefab(item, pendingSource) != null;
|
||||
|
||||
if (generated) onPrefabGenerated?.Invoke();
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -398,21 +465,19 @@ namespace Ashwild.EditorTools
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// Collapses the whole generation card once every prefab the item needs already exists — the
|
||||
/// world pickup for any item, plus the hand prefab for tools/weapons — 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;
|
||||
bool needWorld = worldField.value == null;
|
||||
bool needHand = isToolLike && handField.value == null;
|
||||
|
||||
SetRowVisible(worldGenButton, showWorld);
|
||||
SetRowVisible(handGenButton, showHand);
|
||||
SetRowVisible(generationCard, showWorld || showHand);
|
||||
SetRowVisible(generationCard, needWorld || needHand);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -13,8 +13,8 @@ namespace Ashwild.EditorTools
|
||||
/// 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.
|
||||
/// appends a new ingredient. A compact name field sits above it, and a station card below states
|
||||
/// where the recipe can be crafted. All edits write straight to the asset and repaint the strip.
|
||||
/// </summary>
|
||||
public class RecipeEditorView
|
||||
{
|
||||
@@ -51,6 +51,7 @@ namespace Ashwild.EditorTools
|
||||
Root = new VisualElement();
|
||||
Root.Add(BuildNameField());
|
||||
Root.Add(BuildEquation());
|
||||
Root.Add(BuildStationCard());
|
||||
Root.Add(BuildPickerProxy());
|
||||
|
||||
RefreshEquation();
|
||||
@@ -76,6 +77,28 @@ namespace Ashwild.EditorTools
|
||||
return row;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the card stating where the recipe can be crafted. The hint spells out the meaning of
|
||||
/// None, which reads as "no station" but actually means "craftable everywhere — bare hands and
|
||||
/// every station alike"; any other value restricts the recipe to that station only.
|
||||
/// Changing it refreshes the list row, whose tag shows the station.
|
||||
/// </summary>
|
||||
private VisualElement BuildStationCard()
|
||||
{
|
||||
VisualElement card = AshwildUI.Card("Station");
|
||||
|
||||
EnumField stationField = new EnumField("Crafted On", recipe.RequiredStation);
|
||||
stationField.BindProperty(so.FindProperty("requiredStation"));
|
||||
stationField.RegisterValueChangedCallback(_ => onMetaChanged?.Invoke());
|
||||
card.Add(stationField);
|
||||
|
||||
Label hint = new Label("None = craftable by hand and on every station.");
|
||||
hint.AddToClassList("ash-card__hint");
|
||||
card.Add(hint);
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Equation
|
||||
|
||||
Reference in New Issue
Block a user