diff --git a/S1API/Items/ItemCreator.cs b/S1API/Items/ItemCreator.cs index 7d0a070..b447be6 100644 --- a/S1API/Items/ItemCreator.cs +++ b/S1API/Items/ItemCreator.cs @@ -1,4 +1,14 @@ +using System; +using S1API.Internal.Utils; +using S1API.Leveling; using UnityEngine; +#if (IL2CPPMELON) +using S1ItemFramework = Il2CppScheduleOne.ItemFramework; +using S1Registry = Il2CppScheduleOne.Registry; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1ItemFramework = ScheduleOne.ItemFramework; +using S1Registry = ScheduleOne.Registry; +#endif namespace S1API.Items { @@ -30,6 +40,44 @@ public static StorableItemDefinitionBuilder CreateBuilder() return new StorableItemDefinitionBuilder(); } + /// + /// Creates a new storable item builder by cloning an existing item by ID. + /// + /// The ID of the item to clone. + /// A builder pre-configured with the source item properties. + /// Thrown if the source item ID is not found or is not a storable item. + public static StorableItemDefinitionBuilder CloneFrom(string sourceItemId) + { + var sourceDefinition = S1Registry.GetItem(sourceItemId); + if (sourceDefinition == null) + { + throw new ArgumentException($"Source item with ID '{sourceItemId}' not found in registry", nameof(sourceItemId)); + } + + if (!CrossType.Is(sourceDefinition, out S1ItemFramework.StorableItemDefinition storableDef)) + { + throw new ArgumentException($"Item '{sourceItemId}' is not an StorableItemDefinition", nameof(sourceItemId)); + } + + return new StorableItemDefinitionBuilder(storableDef); + } + + /// + /// Creates a new storable item builder by cloning an existing storable item wrapper. + /// + /// The storable item definition to clone. + /// A builder pre-configured with the source item properties. + /// Thrown if the source definition is null. + public static StorableItemDefinitionBuilder CloneFrom(StorableItemDefinition source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source), "Source storable item definition cannot be null"); + } + + return new StorableItemDefinitionBuilder(source.S1StorableItemDefinition); + } + /// /// Creates an item with common parameters in a single call. /// The item is automatically registered with the game's registry. @@ -42,6 +90,8 @@ public static StorableItemDefinitionBuilder CreateBuilder() /// Base price when buying from shops (default: 10). /// Fraction of purchase price recovered when selling (default: 0.5). /// Whether the item is legal or illegal (default: Legal). + /// Whether purchasing the item requires a certain player rank (default: false). + /// The player rank required to purchase the item, if applicable (default: null). /// Optional sprite to use as the item icon. /// Optional equippable component to attach. /// A wrapper around the created item definition. @@ -66,6 +116,8 @@ public static StorableItemDefinition CreateItem( float basePurchasePrice = 10f, float resellMultiplier = 0.5f, LegalStatus legalStatus = LegalStatus.Legal, + bool requiresLevelToPurchase = false, + FullRank? requiredRank = null, Sprite icon = null, Equippable equippable = null) { @@ -73,6 +125,7 @@ public static StorableItemDefinition CreateItem( .WithBasicInfo(id, name, description, category) .WithStackLimit(stackLimit) .WithPricing(basePurchasePrice, resellMultiplier) + .WithRequiredRank(requiredRank) .WithLegalStatus(legalStatus); if (icon != null) diff --git a/S1API/Items/ItemManager.cs b/S1API/Items/ItemManager.cs index b384dfc..d28260b 100644 --- a/S1API/Items/ItemManager.cs +++ b/S1API/Items/ItemManager.cs @@ -63,6 +63,10 @@ public static ItemDefinition GetItemDefinition(string itemID) out S1ItemFramework.AdditiveDefinition additiveDefinition)) return new AdditiveDefinition(additiveDefinition); + if (CrossType.Is(itemDefinition, + out S1ItemFramework.QualityItemDefinition qualityItemDefinition)) + return new QualityItemDefinition(qualityItemDefinition); + if (CrossType.Is(itemDefinition, out S1ItemFramework.StorableItemDefinition storableItemDefinition)) return new StorableItemDefinition(storableItemDefinition); diff --git a/S1API/Items/QualityItemCreator.cs b/S1API/Items/QualityItemCreator.cs new file mode 100644 index 0000000..3255b15 --- /dev/null +++ b/S1API/Items/QualityItemCreator.cs @@ -0,0 +1,67 @@ +#if (IL2CPPMELON) +using S1ItemFramework = Il2CppScheduleOne.ItemFramework; +using S1Registry = Il2CppScheduleOne.Registry; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1ItemFramework = ScheduleOne.ItemFramework; +using S1Registry = ScheduleOne.Registry; +#endif +using System; +using S1API.Internal.Utils; + +namespace S1API.Items +{ + /// + /// Provides convenient static methods for creating custom quality items. + /// Use for flexible configuration + /// or for quick variants based on existing items. + /// + public class QualityItemCreator + { + /// + /// Creates a new builder for composing a quality item definition with full flexibility. + /// Use fluent methods to configure the definition, then call Build() to register it. + /// + public static QualityItemDefinitionBuilder CreateBuilder() + { + return new QualityItemDefinitionBuilder(); + } + + /// + /// Creates a new quality item builder by cloning an existing quality item by ID. + /// + /// The ID of the item to clone. + /// A builder pre-configured with the source item properties. + /// Thrown if the source item ID is not found or is not a quality item. + public static QualityItemDefinitionBuilder CloneFrom(string sourceItemId) + { + var sourceDefinition = S1Registry.GetItem(sourceItemId); + if (sourceDefinition == null) + { + throw new ArgumentException($"Source item with ID '{sourceItemId}' not found in registry", nameof(sourceItemId)); + } + + if (!CrossType.Is(sourceDefinition, out S1ItemFramework.QualityItemDefinition qualityDef)) + { + throw new ArgumentException($"Item '{sourceItemId}' is not an QualityItemDefinition", nameof(sourceItemId)); + } + + return new QualityItemDefinitionBuilder(qualityDef); + } + + /// + /// Creates a new quality item builder by cloning an existing quality item wrapper. + /// + /// The quality item definition to clone. + /// A builder pre-configured with the source item properties. + /// Thrown if the source definition is null. + public static QualityItemDefinitionBuilder CloneFrom(QualityItemDefinition source) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source), "Source storable item definition cannot be null"); + } + + return new QualityItemDefinitionBuilder(source.S1QualityDefinition); + } + } +} \ No newline at end of file diff --git a/S1API/Items/QualityItemDefinition.cs b/S1API/Items/QualityItemDefinition.cs new file mode 100644 index 0000000..576474c --- /dev/null +++ b/S1API/Items/QualityItemDefinition.cs @@ -0,0 +1,67 @@ +#if (IL2CPPMELON) +using S1ItemFramework = Il2CppScheduleOne.ItemFramework; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1ItemFramework = ScheduleOne.ItemFramework; +#endif +using S1API.Products; + +namespace S1API.Items +{ + /// + /// Represents a quality item definition that can be consumed or used in recipes + /// Extends with quality-specific properties. + /// + /// + /// Use + /// + public class QualityItemDefinition : StorableItemDefinition + { + /// + /// INTERNAL: Wraps an existing native quality item definition. + /// + internal QualityItemDefinition(S1ItemFramework.QualityItemDefinition definition) : base(definition) + { + S1QualityDefinition = definition; + } + + /// + /// INTERNAL: The underlying S1 quality item definition instance. + /// + internal S1ItemFramework.QualityItemDefinition S1QualityDefinition { get; } + + /// + /// Creates a quality item instance from this definition using the default quality. + /// + /// The quantity to apply to the created instance. + /// A quality item instance using this definition's default quality. + public override ItemInstance CreateInstance(int quantity = 1) => CreateInstance(quantity, DefaultQuality); + + /// + /// Creates a quality item instance from this definition with the specified quality. + /// + /// The quality to apply to the created instance. + /// A quality item instance using the specified quality. + public QualityItemInstance CreateInstance(Quality quality) => CreateInstance(1, quality); + + /// + /// Creates a quality item instance from this definition with the specified quantity and quality. + /// + /// The quantity to apply to the created instance. + /// The quality to apply to the created instance. + /// A quality item instance using the specified quantity and quality. + public QualityItemInstance CreateInstance(int quantity, Quality quality) => + new QualityItemInstance(new S1ItemFramework.QualityItemInstance( + S1QualityDefinition, + quantity, + (S1ItemFramework.EQuality)quality)); + + /// + /// The default quality for this item. + /// + public Quality DefaultQuality + { + get => (Quality)S1QualityDefinition.DefaultQuality; + set => S1QualityDefinition.DefaultQuality = (S1ItemFramework.EQuality)value; + } + } +} \ No newline at end of file diff --git a/S1API/Items/QualityItemDefinitionBuilder.cs b/S1API/Items/QualityItemDefinitionBuilder.cs new file mode 100644 index 0000000..db7fd0f --- /dev/null +++ b/S1API/Items/QualityItemDefinitionBuilder.cs @@ -0,0 +1,412 @@ +#if (IL2CPPMELON) +using S1ItemFramework = Il2CppScheduleOne.ItemFramework; +using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework; +using S1Levelling = Il2CppScheduleOne.Levelling; +using S1Registry = Il2CppScheduleOne.Registry; +using S1StationFramework = Il2CppScheduleOne.StationFramework; +using S1Storage = Il2CppScheduleOne.Storage; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1ItemFramework = ScheduleOne.ItemFramework; +using S1CoreItemFramework = ScheduleOne.Core.Items.Framework; +using S1Levelling = ScheduleOne.Levelling; +using S1Registry = ScheduleOne.Registry; +using S1StationFramework = ScheduleOne.StationFramework; +using S1Storage = ScheduleOne.Storage; +#endif + +using System; +using System.Collections.Generic; +using S1API.Logging; +using S1API.Products; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace S1API.Items +{ + /// + /// Builder for composing quality item definitions at runtime. + /// Use fluent methods to configure item properties before calling + /// + public sealed class QualityItemDefinitionBuilder + { + private static readonly Log Logger = new Log("QualityItemDefinitionBuilder"); + private static readonly object StationItemGate = new object(); + private static readonly Dictionary StationItemCache = new Dictionary(); + private static readonly HashSet WarnedStationItemModuleMissing = new HashSet(); + private static GameObject _stationItemRoot; + + private readonly S1ItemFramework.QualityItemDefinition _definition; + private readonly GameObject _storedItemPlaceholder; + private bool _hasCustomStoredItem; + + /// + /// INTERNAL: Creates a new builder instance with a fresh QualityItemDefinition. + /// Only QualityItemCreator can instantiate this. + /// + internal QualityItemDefinitionBuilder() + { + _definition = ScriptableObject.CreateInstance(); + + // Set defaults + _definition.StackLimit = 10; + _definition.BasePurchasePrice = 10f; + _definition.ResellMultiplier = 0.5f; + _definition.Category = S1CoreItemFramework.EItemCategory.Tools; + _definition.legalStatus = S1CoreItemFramework.ELegalStatus.Legal; + _definition.AvailableInDemo = true; + _definition.UsableInFilters = true; + _definition.RequiresLevelToPurchase = false; + _definition.RequiredRank = new S1Levelling.FullRank(S1Levelling.ERank.Street_Rat, 1); + _definition.DefaultQuality = S1ItemFramework.EQuality.Standard; + + // Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors. + _storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem"); + _storedItemPlaceholder.SetActive(false); + _storedItemPlaceholder.hideFlags = HideFlags.HideAndDontSave; + Object.DontDestroyOnLoad(_storedItemPlaceholder); + var storedItemComponent = _storedItemPlaceholder.AddComponent(); + _definition.StoredItem = storedItemComponent; + } + + /// + /// INTERNAL: Creates a builder instance initialized by cloning an existing quality item definition. + /// Only QualityItemCreator can instantiate this. + /// + /// + internal QualityItemDefinitionBuilder(S1ItemFramework.QualityItemDefinition source) + { + _definition = ScriptableObject.CreateInstance(); + + // Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors. + _storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem"); + _storedItemPlaceholder.SetActive(false); + _storedItemPlaceholder.hideFlags = HideFlags.HideAndDontSave; + Object.DontDestroyOnLoad(_storedItemPlaceholder); + var storedItemComponent = _storedItemPlaceholder.AddComponent(); + _definition.StoredItem = storedItemComponent; + + CopyPropertiesFrom(source); + } + + /// + /// Sets the basic information for the item. + /// + /// Unique identifier for the item (e.g., "my_custom_item"). + /// Display name shown in UI. + /// Item description shown in tooltips. + /// Item category for inventory organization. + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithBasicInfo(string id, string name, string description, ItemCategory category) + { + _definition.ID = id; + _definition.Name = name; + _definition.Description = description; + _definition.Category = (S1CoreItemFramework.EItemCategory)category; + + // Update the underlying ScriptableObject name for clarity in inspectors/debuggers. + var displayName = string.IsNullOrEmpty(name) ? id : name; + if (!string.IsNullOrEmpty(displayName)) + { + _definition.name = displayName; + if (_storedItemPlaceholder != null && !_hasCustomStoredItem) + { + _storedItemPlaceholder.name = $"{displayName}_StoredItem"; + } + } + return this; + } + + /// + /// Sets the maximum stack size for this item. + /// + /// Maximum quantity per inventory slot (1-999). + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithStackLimit(int limit) + { + _definition.StackLimit = Mathf.Clamp(limit, 1, 999); + return this; + } + + /// + /// Sets the icon sprite displayed for this item in UI. + /// + /// The sprite to use as the item icon. + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithIcon(Sprite icon) + { + _definition.Icon = icon; + return this; + } + + /// + /// Configures the economic properties of the item. + /// + /// Base price when buying from shops. + /// Fraction of purchase price recovered when selling (0.0 to 1.0). + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithPricing(float basePurchasePrice, float resellMultiplier = 0.5f) + { + _definition.BasePurchasePrice = Mathf.Max(0f, basePurchasePrice); + _definition.ResellMultiplier = Mathf.Clamp01(resellMultiplier); + return this; + } + + /// + /// Sets the legal status of the item. + /// + /// Whether the item is legal or illegal. + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithLegalStatus(LegalStatus status) + { + _definition.legalStatus = (S1CoreItemFramework.ELegalStatus)status; + return this; + } + + /// + /// Attaches an equippable component to this item, allowing it to be equipped by the player. + /// + /// The equippable wrapper to attach. + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithEquippable(Equippable equippable) + { + if (equippable != null) + { + _definition.Equippable = equippable.S1Equippable; + } + return this; + } + + /// + /// Assigns a custom StoredItem prefab for this definition. + /// + /// Prefab containing a StoredItem component. + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithStoredItem(GameObject storedItemPrefab) + { + if (storedItemPrefab == null) + return this; + + var storedItem = storedItemPrefab.GetComponent() ?? storedItemPrefab.AddComponent(); + _definition.StoredItem = storedItem; + _hasCustomStoredItem = true; + return this; + } + + /// + /// Assigns a StationItem prefab to this item definition so it can be used as a station/minigame ingredient + /// (e.g., Chemistry Station). + /// + /// + /// S1API clones and caches the prefab under a hidden DontDestroyOnLoad root by default. + /// This avoids mutating shared prefabs and helps keep the reference stable across scene loads. + /// + /// A prefab GameObject that has a StationItem component. + /// The builder instance for fluent chaining. + /// Thrown if is null. + /// Thrown if does not have a StationItem component. + public QualityItemDefinitionBuilder WithStationItem(GameObject stationItemPrefab) + { + if (stationItemPrefab == null) + throw new ArgumentNullException(nameof(stationItemPrefab)); + + var stationItem = stationItemPrefab.GetComponent(); + if (stationItem == null) + throw new ArgumentException("Station item prefab must have a StationItem component.", nameof(stationItemPrefab)); + + var cached = GetOrCreateStationItemPrefab(stationItem); + _definition.StationItem = cached; + + WarnIfStationItemMissingChemistryModules(cached); + return this; + } + + /// + /// Clears the StationItem reference for this definition. + /// + public QualityItemDefinitionBuilder WithoutStationItem() + { + _definition.StationItem = null; + return this; + } + + /// + /// Sets whether this item is available in the demo version of the game. + /// + /// True if available in demo, false otherwise. + /// The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithDemoAvailability(bool available) + { + _definition.AvailableInDemo = available; + return this; + } + + /// + /// Assigns a level requirement for purchasing this item in shops. + /// + /// The required rank to purchase this item, or null to remove level requirement. + /// >The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithRequiredRank(Leveling.FullRank? rank) + { + if (rank == null) + { + _definition.RequiresLevelToPurchase = false; + return this; + } + _definition.RequiredRank = rank.Value.ToNative(); + _definition.RequiresLevelToPurchase = true; + return this; + } + + /// + /// Assigns a default quality for this definition. + /// + /// The default quality to assign to items of this definition. + /// >The builder instance for fluent chaining. + public QualityItemDefinitionBuilder WithDefaultQuality(Quality quality) + { + _definition.DefaultQuality = (S1ItemFramework.EQuality)quality; + return this; + } + + /// + /// Builds the item definition, registers it with the game's registry, and returns a wrapper. + /// + /// A wrapper around the created storable item definition. + public QualityItemDefinition Build() + { + if (!_hasCustomStoredItem && _definition.StoredItem != null) + { + // Ensure placeholder naming stays in sync after late changes. + if (!string.IsNullOrEmpty(_definition.Name) && _storedItemPlaceholder != null) + { + _storedItemPlaceholder.name = $"{_definition.Name}_StoredItem"; + } + } + + // Register with the game's registry + S1Registry.Instance.AddToRegistry(_definition); + + // Return wrapper + return new QualityItemDefinition(_definition); + } + + /// + /// INTERNAL: Builds and returns the raw game item definition without registering. + /// Used internally by S1API. Modders should use instead. + /// + internal S1ItemFramework.QualityItemDefinition BuildInternal() + { + return _definition; + } + + /// + /// Copies all properties from a source StorableItemDefinition to the current definition. + /// + /// The source definition to copy properties from. + private void CopyPropertiesFrom(S1ItemFramework.QualityItemDefinition source) + { + if (source == null) return; + + // Basic ItemDefinition properties + _definition.Name = source.Name; + _definition.Description = source.Description; + _definition.Category = source.Category; + _definition.StackLimit = source.StackLimit; + _definition.AvailableInDemo = source.AvailableInDemo; + _definition.UsableInFilters = source.UsableInFilters; + _definition.Icon = source.Icon; + _definition.legalStatus = source.legalStatus; + _definition.PickpocketDifficultyMultiplier = source.PickpocketDifficultyMultiplier; + _definition.CombatUtility = source.CombatUtility; + + // StorableItemDefinition properties + _definition.BasePurchasePrice = source.BasePurchasePrice; + _definition.ResellMultiplier = source.ResellMultiplier; + _definition.ShopCategories = source.ShopCategories; + _definition.RequiresLevelToPurchase = source.RequiresLevelToPurchase; + _definition.RequiredRank = source.RequiredRank; + _definition.StoredItem = source.StoredItem != null ? source.StoredItem : _definition.StoredItem; + _definition.StationItem = source.StationItem; + _definition.Equippable = source.Equippable; + _definition.DefaultQuality = source.DefaultQuality; + _definition.CustomItemUI = source.CustomItemUI; + } + + private static S1StationFramework.StationItem GetOrCreateStationItemPrefab(S1StationFramework.StationItem stationItemPrefab) + { + var id = stationItemPrefab.GetInstanceID(); + + lock (StationItemGate) + { + if (StationItemCache.TryGetValue(id, out var cached) && cached != null) + return cached; + + var root = GetStationItemRoot(); + + // Clone + cache (final decision): keep a stable hidden prefab reference across scene loads. + var clone = Object.Instantiate(stationItemPrefab, root.transform); + clone.gameObject.hideFlags = HideFlags.HideAndDontSave; + clone.name = $"{stationItemPrefab.name}_S1API_StationItem"; + + // Keep the cache far away from gameplay so it doesn't interfere with scenes. + clone.transform.position = root.transform.position; + + StationItemCache[id] = clone; + return clone; + } + } + + private static GameObject GetStationItemRoot() + { + if (_stationItemRoot != null) + return _stationItemRoot; + + lock (StationItemGate) + { + if (_stationItemRoot != null) + return _stationItemRoot; + + var root = new GameObject("S1API_StationItemCache"); + root.hideFlags = HideFlags.HideAndDontSave; + Object.DontDestroyOnLoad(root); + + // Place it far below the world; keep it active so instantiated prefabs remain active by default. + root.transform.position = new Vector3(0f, -10000f, 0f); + + _stationItemRoot = root; + return root; + } + } + + private static void WarnIfStationItemMissingChemistryModules(S1StationFramework.StationItem stationItemPrefab) + { + if (stationItemPrefab == null) + return; + + var id = stationItemPrefab.GetInstanceID(); + + lock (StationItemGate) + { + if (!WarnedStationItemModuleMissing.Add(id)) + return; + } + + try + { + var hasIngredientModule = stationItemPrefab.GetComponentInChildren(true) != null; + var hasPourableModule = stationItemPrefab.GetComponentInChildren(true) != null; + + if (hasIngredientModule || hasPourableModule) + return; + + Logger.Warning( + $"[S1API] StationItem prefab '{stationItemPrefab.name}' does not contain an IngredientModule or PourableModule. " + + "Chemistry station tasks may log errors or skip this ingredient at runtime."); + } + catch + { + // best-effort warning only + } + } + } +} diff --git a/S1API/Items/QualityItemInstance.cs b/S1API/Items/QualityItemInstance.cs new file mode 100644 index 0000000..b78ec11 --- /dev/null +++ b/S1API/Items/QualityItemInstance.cs @@ -0,0 +1,45 @@ +#if (IL2CPPMELON) +using S1ItemFramework = Il2CppScheduleOne.ItemFramework; +#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) +using S1ItemFramework = ScheduleOne.ItemFramework; +#endif +using S1API.Products; + +namespace S1API.Items +{ + /// + /// Represents a quality item instance in the game world (usable item). + /// Extends with quality information. + /// + public class QualityItemInstance : ItemInstance + { + /// + /// INTERNAL: Reference to the in-game quality item instance. + /// + internal readonly S1ItemFramework.QualityItemInstance S1QualityInstance; + + /// + /// INTERNAL: Creates a QualityItemInstance wrapper. + /// + /// In-game quality item instance + internal QualityItemInstance(S1ItemFramework.QualityItemInstance itemInstance) : base(itemInstance) + { + S1QualityInstance = itemInstance; + } + + /// + /// The quality of this item. + /// + public Quality Quality + { + get => (Quality)S1QualityInstance.Quality; + set => S1QualityInstance.Quality = (S1ItemFramework.EQuality)value; + } + + /// + /// The quality item definition (template) this instance was created from. + /// + public new QualityItemDefinition Definition => + new QualityItemDefinition((S1ItemFramework.QualityItemDefinition)S1QualityInstance.Definition); + } +} \ No newline at end of file diff --git a/S1API/Items/StorableItemDefinition.cs b/S1API/Items/StorableItemDefinition.cs index 5760758..0e3701c 100644 --- a/S1API/Items/StorableItemDefinition.cs +++ b/S1API/Items/StorableItemDefinition.cs @@ -4,6 +4,7 @@ using S1ItemFramework = ScheduleOne.ItemFramework; #endif +using S1API.Leveling; using UnityEngine; namespace S1API.Items @@ -57,6 +58,24 @@ public float ResellMultiplier public bool IsUnlocked => S1StorableItemDefinition.IsUnlocked; + /// + /// Whether purchasing this item requires the player to be at or above a certain level. + /// + public bool RequiresLevelToPurchase + { + get => S1StorableItemDefinition.RequiresLevelToPurchase; + set => S1StorableItemDefinition.RequiresLevelToPurchase = value; + } + + /// + /// The required player level to purchase this item, if is true. + /// + public FullRank RequiredRank + { + get => FullRank.FromNative(S1StorableItemDefinition.RequiredRank); + set => S1StorableItemDefinition.RequiredRank = value.ToNative(); + } + /// /// Gets whether this item has a StationItem assigned (used by station/minigame tasks, e.g., Chemistry Station). /// diff --git a/S1API/Items/StorableItemDefinitionBuilder.cs b/S1API/Items/StorableItemDefinitionBuilder.cs index 4f36e79..91c66a1 100644 --- a/S1API/Items/StorableItemDefinitionBuilder.cs +++ b/S1API/Items/StorableItemDefinitionBuilder.cs @@ -1,12 +1,14 @@ #if (IL2CPPMELON) using S1ItemFramework = Il2CppScheduleOne.ItemFramework; using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework; +using S1Levelling = Il2CppScheduleOne.Levelling; using S1Registry = Il2CppScheduleOne.Registry; using S1StationFramework = Il2CppScheduleOne.StationFramework; using S1Storage = Il2CppScheduleOne.Storage; #elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX) using S1ItemFramework = ScheduleOne.ItemFramework; using S1CoreItemFramework = ScheduleOne.Core.Items.Framework; +using S1Levelling = ScheduleOne.Levelling; using S1Registry = ScheduleOne.Registry; using S1StationFramework = ScheduleOne.StationFramework; using S1Storage = ScheduleOne.Storage; @@ -47,7 +49,7 @@ public sealed class StorableItemDefinitionBuilder internal StorableItemDefinitionBuilder() { _definition = ScriptableObject.CreateInstance(); - + // Set defaults _definition.StackLimit = 10; _definition.BasePurchasePrice = 10f; @@ -56,6 +58,26 @@ internal StorableItemDefinitionBuilder() _definition.legalStatus = S1CoreItemFramework.ELegalStatus.Legal; _definition.AvailableInDemo = true; _definition.UsableInFilters = true; + _definition.RequiresLevelToPurchase = false; + _definition.RequiredRank = new S1Levelling.FullRank(S1Levelling.ERank.Street_Rat, 1); + + // Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors. + _storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem"); + _storedItemPlaceholder.SetActive(false); + _storedItemPlaceholder.hideFlags = HideFlags.HideAndDontSave; + Object.DontDestroyOnLoad(_storedItemPlaceholder); + var storedItemComponent = _storedItemPlaceholder.AddComponent(); + _definition.StoredItem = storedItemComponent; + } + + /// + /// INTERNAL: Creates a builder instance initialized by cloning an existing storable item definition. + /// Only ItemCreator can instantiate this. + /// + /// The source definition to clone properties from. + internal StorableItemDefinitionBuilder(S1ItemFramework.StorableItemDefinition source) + { + _definition = ScriptableObject.CreateInstance(); // Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors. _storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem"); @@ -64,6 +86,8 @@ internal StorableItemDefinitionBuilder() Object.DontDestroyOnLoad(_storedItemPlaceholder); var storedItemComponent = _storedItemPlaceholder.AddComponent(); _definition.StoredItem = storedItemComponent; + + CopyPropertiesFrom(source); } /// @@ -218,6 +242,24 @@ public StorableItemDefinitionBuilder WithDemoAvailability(bool available) return this; } + /// + /// Assigns a level requirement for purchasing this item in shops. + /// + /// The required rank to purchase this item, or null to remove level requirement. + /// >The builder instance for fluent chaining. + public StorableItemDefinitionBuilder WithRequiredRank(Leveling.FullRank? rank) + { + if (rank == null) + { + _definition.RequiresLevelToPurchase = false; + return this; + } + + _definition.RequiredRank = rank.Value.ToNative(); + _definition.RequiresLevelToPurchase = true; + return this; + } + /// /// Builds the item definition, registers it with the game's registry, and returns a wrapper. /// @@ -249,7 +291,39 @@ internal S1ItemFramework.StorableItemDefinition BuildInternal() return _definition; } - private static S1StationFramework.StationItem GetOrCreateStationItemPrefab(S1StationFramework.StationItem stationItemPrefab) + /// + /// Copies all properties from a source StorableItemDefinition to the current definition. + /// + /// The source definition to copy properties from. + private void CopyPropertiesFrom(S1ItemFramework.StorableItemDefinition source) + { + if (source == null) return; + + // Basic ItemDefinition properties + _definition.Name = source.Name; + _definition.Description = source.Description; + _definition.Category = source.Category; + _definition.StackLimit = source.StackLimit; + _definition.AvailableInDemo = source.AvailableInDemo; + _definition.UsableInFilters = source.UsableInFilters; + _definition.Icon = source.Icon; + _definition.legalStatus = source.legalStatus; + _definition.PickpocketDifficultyMultiplier = source.PickpocketDifficultyMultiplier; + _definition.CombatUtility = source.CombatUtility; + + // StorableItemDefinition properties + _definition.BasePurchasePrice = source.BasePurchasePrice; + _definition.ResellMultiplier = source.ResellMultiplier; + _definition.ShopCategories = source.ShopCategories; + _definition.RequiresLevelToPurchase = source.RequiresLevelToPurchase; + _definition.RequiredRank = source.RequiredRank; + _definition.StoredItem = source.StoredItem != null ? source.StoredItem : _definition.StoredItem; + _definition.StationItem = source.StationItem; + _definition.Equippable = source.Equippable; + } + + private static S1StationFramework.StationItem GetOrCreateStationItemPrefab( + S1StationFramework.StationItem stationItemPrefab) { var id = stationItemPrefab.GetInstanceID(); @@ -326,4 +400,4 @@ private static void WarnIfStationItemMissingChemistryModules(S1StationFramework. } } } -} +} \ No newline at end of file