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
@@ -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>