(Feat) Tools Creation
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Ashwild.Inventory;
|
||||
using Ashwild.Network;
|
||||
using Ashwild.Player;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
public static class ItemAssetFactory
|
||||
{
|
||||
#region Constants
|
||||
|
||||
private const string ItemsFolder = "Assets/GAME/ScriptableObjects/Items";
|
||||
private const string WorldPrefabFolder = "Assets/GAME/Prefabs/Pickable";
|
||||
private const string HandPrefabFolder = "Assets/GAME/Prefabs/Tools";
|
||||
|
||||
private const string PickableLayerName = "Pickable";
|
||||
private const string ToolsLayerName = "Tools";
|
||||
private const string HarvestableLayerName = "Harvestable";
|
||||
|
||||
private const float FallbackColliderSize = 0.3f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Item Asset
|
||||
|
||||
/// <summary>
|
||||
/// Creates a fresh ItemData asset under the items folder with a unique name, seeds its
|
||||
/// display name and type, registers it in the network ItemDatabase, and returns it so the
|
||||
/// caller can select and edit it immediately. Returns null only if the folder cannot be made.
|
||||
/// </summary>
|
||||
public static ItemData CreateItem(string desiredName, ItemType itemType)
|
||||
{
|
||||
if (!EnsureFolder(ItemsFolder)) return null;
|
||||
|
||||
string safeName = string.IsNullOrWhiteSpace(desiredName) ? "NewItem" : desiredName.Trim();
|
||||
string assetPath = AssetDatabase.GenerateUniqueAssetPath($"{ItemsFolder}/{safeName}.asset");
|
||||
|
||||
ItemData item = ScriptableObject.CreateInstance<ItemData>();
|
||||
AssetDatabase.CreateAsset(item, assetPath);
|
||||
|
||||
SerializedObject so = new SerializedObject(item);
|
||||
so.FindProperty("itemName").stringValue = safeName;
|
||||
so.FindProperty("itemType").enumValueIndex = (int)itemType;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
EditorUtility.SetDirty(item);
|
||||
AssetDatabase.SaveAssets();
|
||||
ItemDatabaseBuilder.Rebuild();
|
||||
return item;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#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.
|
||||
/// </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));
|
||||
FitBoxCollider(root);
|
||||
|
||||
Pickable pickable = root.AddComponent<Pickable>();
|
||||
SerializedObject so = new SerializedObject(pickable);
|
||||
so.FindProperty("itemData").objectReferenceValue = item;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
return SaveAndAssign(root, WorldPrefabFolder, item, "worldPrefab");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#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.
|
||||
/// </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));
|
||||
|
||||
ToolBehaviour tool = root.AddComponent<ToolBehaviour>();
|
||||
SerializedObject so = new SerializedObject(tool);
|
||||
so.FindProperty("harvestMask").intValue = 1 << ResolveLayer(HarvestableLayerName);
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
|
||||
root.AddComponent<HeldItemOffset>();
|
||||
|
||||
return SaveAndAssign(root, HandPrefabFolder, item, "handPrefab");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Internal Helpers
|
||||
|
||||
/// <summary>
|
||||
/// Guards the shared preconditions for prefab generation, logging a clear reason on failure.
|
||||
/// </summary>
|
||||
private static bool ValidateInputs(ItemData item, GameObject source, string kind)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
Debug.LogError($"[ItemAssetFactory] Cannot build {kind} prefab — no ItemData selected.");
|
||||
return false;
|
||||
}
|
||||
if (source == null)
|
||||
{
|
||||
Debug.LogError($"[ItemAssetFactory] Cannot build {kind} prefab for '{item.name}' — no source object chosen.", item);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
private static GameObject BuildRoot(ItemData item, GameObject source, int layer)
|
||||
{
|
||||
GameObject root = new GameObject(item.name) { layer = layer };
|
||||
|
||||
GameObject visual = (GameObject)PrefabUtility.InstantiatePrefab(source);
|
||||
if (visual == null) visual = Object.Instantiate(source);
|
||||
visual.transform.SetParent(root.transform, false);
|
||||
visual.transform.localPosition = Vector3.zero;
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a non-trigger BoxCollider to the root sized to the combined bounds of the source's
|
||||
/// renderers (in root-local space), falling back to a small default cube when the source has
|
||||
/// no renderers so the pickup is always interactable.
|
||||
/// </summary>
|
||||
private static void FitBoxCollider(GameObject root)
|
||||
{
|
||||
BoxCollider box = root.AddComponent<BoxCollider>();
|
||||
|
||||
Renderer[] renderers = root.GetComponentsInChildren<Renderer>();
|
||||
if (renderers.Length == 0)
|
||||
{
|
||||
box.size = Vector3.one * FallbackColliderSize;
|
||||
return;
|
||||
}
|
||||
|
||||
Bounds bounds = renderers[0].bounds;
|
||||
for (int i = 1; i < renderers.Length; i++) bounds.Encapsulate(renderers[i].bounds);
|
||||
|
||||
box.center = root.transform.InverseTransformPoint(bounds.center);
|
||||
box.size = bounds.size;
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
private static GameObject SaveAndAssign(GameObject root, string folder, ItemData item, string itemField)
|
||||
{
|
||||
string prefabPath = AssetDatabase.GenerateUniqueAssetPath($"{folder}/{item.name}.prefab");
|
||||
GameObject prefab = PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
|
||||
Object.DestroyImmediate(root);
|
||||
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError($"[ItemAssetFactory] Failed to save prefab at '{prefabPath}'.", item);
|
||||
return null;
|
||||
}
|
||||
|
||||
SerializedObject so = new SerializedObject(item);
|
||||
so.FindProperty(itemField).objectReferenceValue = prefab;
|
||||
so.ApplyModifiedPropertiesWithoutUndo();
|
||||
EditorUtility.SetDirty(item);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
Debug.Log($"[ItemAssetFactory] Built '{prefabPath}' and assigned it to {item.name}.{itemField}.", prefab);
|
||||
return prefab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a layer index by name, falling back to the Default layer (0) with a warning when
|
||||
/// the project is missing the expected layer so generation never silently lands on a wrong one.
|
||||
/// </summary>
|
||||
private static int ResolveLayer(string layerName)
|
||||
{
|
||||
int layer = LayerMask.NameToLayer(layerName);
|
||||
if (layer >= 0) return layer;
|
||||
|
||||
Debug.LogWarning($"[ItemAssetFactory] Layer '{layerName}' not found — using Default. Add it in the Tags & Layers settings.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures a project-relative asset folder exists, creating any missing segments. Returns
|
||||
/// false (and logs) when the path cannot be created.
|
||||
/// </summary>
|
||||
private static bool EnsureFolder(string folder)
|
||||
{
|
||||
if (AssetDatabase.IsValidFolder(folder)) return true;
|
||||
|
||||
string parent = Path.GetDirectoryName(folder).Replace('\\', '/');
|
||||
string leaf = Path.GetFileName(folder);
|
||||
if (!EnsureFolder(parent))
|
||||
{
|
||||
Debug.LogError($"[ItemAssetFactory] Could not create folder '{folder}'.");
|
||||
return false;
|
||||
}
|
||||
|
||||
AssetDatabase.CreateFolder(parent, leaf);
|
||||
return AssetDatabase.IsValidFolder(folder);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user