using System.IO; using UnityEditor; using UnityEngine; using Ashwild.Inventory; using Ashwild.Network; using Ashwild.Player; namespace Ashwild.EditorTools { /// /// 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. /// 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 /// /// 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. /// 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(); 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 /// /// 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. /// 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(); SerializedObject so = new SerializedObject(pickable); so.FindProperty("itemData").objectReferenceValue = item; so.ApplyModifiedPropertiesWithoutUndo(); return SaveAndAssign(root, WorldPrefabFolder, item, "worldPrefab"); } #endregion #region Hand Prefab /// /// 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. /// 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(); SerializedObject so = new SerializedObject(tool); so.FindProperty("harvestMask").intValue = 1 << ResolveLayer(HarvestableLayerName); so.ApplyModifiedPropertiesWithoutUndo(); root.AddComponent(); return SaveAndAssign(root, HandPrefabFolder, item, "handPrefab"); } #endregion #region Internal Helpers /// /// Guards the shared preconditions for prefab generation, logging a clear reason on failure. /// 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; } /// /// 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. /// 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; } /// /// 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. /// private static void FitBoxCollider(GameObject root) { BoxCollider box = root.AddComponent(); Renderer[] renderers = root.GetComponentsInChildren(); 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; } /// /// 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. /// 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; } /// /// 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. /// 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; } /// /// Ensures a project-relative asset folder exists, creating any missing segments. Returns /// false (and logs) when the path cannot be created. /// 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 } }