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:
2026-07-25 19:42:26 +02:00
954 changed files with 999772 additions and 26193 deletions
@@ -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