2026-06-13 17:22:03 +01:00
/ * This Source Code Form is subject to the terms of the Mozilla Public
2020-12-22 03:13:43 +00:00
* License , v . 2.0 . If a copy of the MPL was not distributed with this
* file , You can obtain one at https : //mozilla.org/MPL/2.0/. */
using System ;
2020-03-22 19:05:26 +00:00
using System.Collections ;
using System.Collections.Generic ;
using System.Linq ;
2026-06-14 10:54:17 +01:00
using System.Reflection ;
2026-06-14 04:35:51 +01:00
using GameCult.Aetheria.State.Unity ;
2020-05-04 00:07:36 +01:00
using Unity.Mathematics ;
2020-03-22 19:05:26 +00:00
using static Unity . Mathematics . math ;
2020-05-04 00:07:36 +01:00
using float2 = Unity . Mathematics . float2 ;
2020-04-30 02:54:31 +01:00
using Random = Unity . Mathematics . Random ;
2020-05-10 00:16:33 +01:00
using JM.LinqFaster ;
2020-12-01 00:16:11 +00:00
using UniRx ;
2020-05-30 02:44:22 +01:00
using float4 = Unity . Mathematics . float4 ;
2020-03-22 19:05:26 +00:00
2020-12-10 23:20:38 +00:00
public class ItemManager
2020-03-22 19:05:26 +00:00
{
2020-04-30 02:54:31 +01:00
public Random Random = new Random ( ( uint ) ( DateTime . Now . Ticks % uint . MaxValue ) ) ;
2020-12-10 23:20:38 +00:00
// public SimpleCommodityData[] Resources;
// public Dictionary<Guid, List<IController>> CorporationControllers = new Dictionary<Guid, List<IController>>();
// public Dictionary<Guid, ZoneDefinition> GalaxyZones;
2020-04-23 03:31:17 +01:00
2020-04-09 07:48:45 +01:00
private Action < string > _logger ;
2020-04-16 03:08:18 +01:00
private double _time ;
private float _deltaTime ;
2020-11-27 22:47:33 +00:00
private Dictionary < Guid , Zone > _zones = new Dictionary < Guid , Zone > ( ) ;
2020-12-20 03:04:52 +00:00
2020-11-27 22:47:33 +00:00
// private Guid _forceLoadZone;
2020-05-12 01:42:13 +01:00
2026-06-14 09:19:53 +01:00
private readonly IRuntimeItemCatalogReader _runtimeItems ;
2026-06-14 10:54:17 +01:00
private static readonly IReadOnlyDictionary < string , Type > BehaviorTypesByKind = new Dictionary < string , Type > ( StringComparer . Ordinal )
{
{ "GuidedWeapon" , typeof ( GuidedWeaponData ) } ,
{ "Launcher" , typeof ( LauncherData ) } ,
{ "Reactor" , typeof ( ReactorData ) } ,
{ "Radiator" , typeof ( RadiatorData ) } ,
{ "StatModifier" , typeof ( StatModifierData ) } ,
{ "Sensor" , typeof ( SensorData ) } ,
{ "Reflector" , typeof ( ReflectorData ) } ,
{ "Shield" , typeof ( ShieldData ) } ,
{ "Thruster" , typeof ( ThrusterData ) } ,
{ "Wear" , typeof ( WearData ) } ,
{ "VelocityConversion" , typeof ( VelocityConversionData ) } ,
{ "VelocityLimit" , typeof ( VelocityLimitData ) } ,
{ "AetherDrive" , typeof ( AetherDriveData ) } ,
{ "Cooldown" , typeof ( CooldownData ) } ,
{ "Heat" , typeof ( HeatData ) } ,
{ "ItemUsage" , typeof ( ItemUsageData ) } ,
{ "Switch" , typeof ( SwitchData ) } ,
{ "Trigger" , typeof ( TriggerData ) } ,
{ "Visibility" , typeof ( VisibilityData ) } ,
{ "Thermotoggle" , typeof ( ThermotoggleData ) } ,
{ "EnergyDraw" , typeof ( EnergyDrawData ) } ,
{ "MiningTool" , typeof ( MiningToolData ) } ,
{ "ResourceScanner" , typeof ( ResourceScannerData ) } ,
{ "Capacitor" , typeof ( CapacitorData ) } ,
{ "Cockpit" , typeof ( CockpitData ) } ,
{ "HeatStorage" , typeof ( HeatStorageData ) } ,
{ "TurretController" , typeof ( TurretControllerData ) } ,
{ "InstantWeapon" , typeof ( InstantWeaponData ) } ,
{ "ConstantWeapon" , typeof ( ConstantWeaponData ) } ,
{ "ChargedWeapon" , typeof ( ChargedWeaponData ) } ,
{ "AutoWeapon" , typeof ( AutoWeaponData ) }
} ;
2020-12-10 23:20:38 +00:00
public GameplaySettings GameplaySettings { get ; }
2020-05-10 00:16:33 +01:00
2021-04-09 14:01:34 +01:00
// public double Time
// {
// get => _time;
// set
// {
// _deltaTime = (float) (value - _time);
// _time = value;
// //Log($"GameContext delta time: {_deltaTime}");
// }
// }
2020-04-16 03:08:18 +01:00
2020-04-09 07:48:45 +01:00
// private readonly Dictionary<CraftedItemData, int> Tier = new Dictionary<CraftedItemData, int>();
2020-03-27 01:53:03 +00:00
2026-06-14 09:19:53 +01:00
public ItemManager ( IRuntimeItemCatalogReader runtimeItems , GameplaySettings settings , Action < string > logger )
2020-03-27 01:53:03 +00:00
{
2026-06-14 09:19:53 +01:00
_runtimeItems = runtimeItems ;
2020-12-20 03:04:52 +00:00
GameplaySettings = settings ;
2020-04-09 07:48:45 +01:00
_logger = logger ;
2020-04-16 03:08:18 +01:00
}
2026-06-14 04:35:51 +01:00
public AetheriaRuntimeCatalogItem GetRuntimeItem ( ItemInstance item )
{
2026-06-14 11:12:48 +01:00
return GetRuntimeItem ( item ? . ItemId ? ? Guid . Empty ) ;
2026-06-14 05:25:56 +01:00
}
public AetheriaRuntimeCatalogItem GetRuntimeItem ( Guid itemId )
{
2026-06-14 09:19:53 +01:00
return itemId = = Guid . Empty ? null : _runtimeItems . GetRuntimeItem ( itemId ) ;
2026-06-14 04:35:51 +01:00
}
2026-06-14 10:29:17 +01:00
public Behavior [ ] CreateRuntimeBehaviors ( EquippedItem item )
{
return CreateRuntimeBehaviorConfigs ( item ? . EquippableItem )
. Select ( config = > config . CreateInstance ( item ) )
. ToArray ( ) ;
}
public Behavior [ ] CreateRuntimeBehaviors ( ConsumableItemEffect effect )
{
return CreateRuntimeBehaviorConfigs ( effect ? . Item )
. Select ( config = > config . CreateInstance ( effect ) )
. ToArray ( ) ;
}
private IReadOnlyList < BehaviorData > CreateRuntimeBehaviorConfigs ( ItemInstance item )
{
2026-06-14 10:54:17 +01:00
var typedItem = GetRuntimeItem ( item ) ;
return typedItem = = null
2026-06-14 10:38:04 +01:00
? Array . Empty < BehaviorData > ( )
2026-06-14 10:54:17 +01:00
: BuildBehaviorConfigs ( typedItem ) ;
}
private static BehaviorData [ ] BuildBehaviorConfigs ( AetheriaRuntimeCatalogItem item )
{
return item . BehaviorPayloads
. Select ( BuildBehaviorConfig )
. Where ( behavior = > behavior ! = null )
. ToArray ( ) ;
}
private static BehaviorData BuildBehaviorConfig ( AetheriaRuntimeBehaviorPayload payload )
{
if ( ! BehaviorTypesByKind . TryGetValue ( payload . Kind , out var behaviorType ) )
{
return null ;
}
var behavior = Activator . CreateInstance ( behaviorType ) as BehaviorData ;
if ( behavior = = null )
{
return null ;
}
behavior . Kind = payload . Kind ;
behavior . Group = payload . Group ;
foreach ( var field in GetKeyedFields ( behaviorType ) )
{
var payloadField = payload . Fields . FirstOrDefault ( candidate = > candidate . Key = = field . key ) ;
if ( payloadField = = null )
{
continue ;
}
field . field . SetValue ( behavior , ConvertValue ( payloadField . Value , field . field . FieldType ) ) ;
}
return behavior ;
}
private static IEnumerable < ( int key , FieldInfo field ) > GetKeyedFields ( Type type )
{
return type
. GetFields ( BindingFlags . Instance | BindingFlags . Public | BindingFlags . NonPublic )
. Select ( field = > ( attribute : field . GetCustomAttribute < LegacyPayloadKeyAttribute > ( ) , field ) )
. Where ( entry = > entry . attribute ! = null )
. Select ( entry = > ( entry . attribute . Key , entry . field ) ) ;
}
private static object ConvertValue ( AetheriaRuntimeBehaviorValue value , Type targetType )
{
if ( targetType = = typeof ( string ) ) return value . StringValue ? ? "" ;
if ( targetType = = typeof ( bool ) ) return value . BoolValue ;
if ( targetType = = typeof ( float ) ) return ( float ) value . NumberValue ;
if ( targetType = = typeof ( double ) ) return value . NumberValue ;
if ( targetType = = typeof ( int ) ) return checked ( ( int ) value . NumberValue ) ;
if ( targetType = = typeof ( uint ) ) return checked ( ( uint ) value . NumberValue ) ;
if ( targetType = = typeof ( Guid ) ) return ParseGuid ( value . LegacyIdValue ) ;
if ( targetType = = typeof ( float2 ) ) return ConvertFloat2 ( value ) ;
if ( targetType = = typeof ( float3 ) ) return ConvertFloat3 ( value ) ;
if ( targetType = = typeof ( float4 ) ) return ConvertFloat4 ( value ) ;
if ( targetType = = typeof ( int2 ) ) return ConvertInt2 ( value ) ;
if ( targetType . IsEnum ) return ParseEnumValue ( value , targetType ) ;
if ( targetType . IsArray )
{
return ConvertArray ( value , targetType . GetElementType ( ) ) ;
}
if ( typeof ( IList ) . IsAssignableFrom ( targetType ) & & targetType . IsGenericType )
{
return ConvertList ( value , targetType ) ;
}
if ( GetKeyedFields ( targetType ) . Any ( ) )
{
return ConvertKeyedObject ( value , targetType ) ;
}
return targetType . IsValueType ? Activator . CreateInstance ( targetType ) : null ;
}
private static object ConvertArray ( AetheriaRuntimeBehaviorValue value , Type elementType )
{
var array = Array . CreateInstance ( elementType , value . Children . Count ) ;
for ( var index = 0 ; index < value . Children . Count ; index + + )
{
array . SetValue ( ConvertValue ( value . Children [ index ] , elementType ) , index ) ;
}
return array ;
}
private static object ConvertList ( AetheriaRuntimeBehaviorValue value , Type targetType )
{
var elementType = targetType . GetGenericArguments ( ) [ 0 ] ;
var list = Activator . CreateInstance ( targetType ) as IList ;
foreach ( var child in value . Children )
{
list . Add ( ConvertValue ( child , elementType ) ) ;
}
return list ;
}
private static object ConvertKeyedObject ( AetheriaRuntimeBehaviorValue value , Type targetType )
{
var instance = Activator . CreateInstance ( targetType ) ;
var children = value . Children ;
foreach ( var field in GetKeyedFields ( targetType ) )
{
if ( field . key > = 0 & & field . key < children . Count )
{
field . field . SetValue ( instance , ConvertValue ( children [ field . key ] , field . field . FieldType ) ) ;
}
}
return instance ;
}
private static object ParseEnumValue ( AetheriaRuntimeBehaviorValue value , Type targetType )
{
if ( ! string . IsNullOrWhiteSpace ( value . StringValue ) )
{
return Enum . Parse ( targetType , value . StringValue , true ) ;
}
return Enum . ToObject ( targetType , checked ( ( int ) value . NumberValue ) ) ;
}
private static Guid ParseGuid ( string value )
{
return Guid . TryParse ( value , out var result ) ? result : Guid . Empty ;
}
private static float2 ConvertFloat2 ( AetheriaRuntimeBehaviorValue value )
{
return new float2 (
value . Children . Count > 0 ? ( float ) value . Children [ 0 ] . NumberValue : 0 ,
value . Children . Count > 1 ? ( float ) value . Children [ 1 ] . NumberValue : 0 ) ;
}
private static float3 ConvertFloat3 ( AetheriaRuntimeBehaviorValue value )
{
return new float3 (
value . Children . Count > 0 ? ( float ) value . Children [ 0 ] . NumberValue : 0 ,
value . Children . Count > 1 ? ( float ) value . Children [ 1 ] . NumberValue : 0 ,
value . Children . Count > 2 ? ( float ) value . Children [ 2 ] . NumberValue : 0 ) ;
}
private static float4 ConvertFloat4 ( AetheriaRuntimeBehaviorValue value )
{
return new float4 (
value . Children . Count > 0 ? ( float ) value . Children [ 0 ] . NumberValue : 0 ,
value . Children . Count > 1 ? ( float ) value . Children [ 1 ] . NumberValue : 0 ,
value . Children . Count > 2 ? ( float ) value . Children [ 2 ] . NumberValue : 0 ,
value . Children . Count > 3 ? ( float ) value . Children [ 3 ] . NumberValue : 0 ) ;
}
private static int2 ConvertInt2 ( AetheriaRuntimeBehaviorValue value )
{
return new int2 (
value . Children . Count > 0 ? checked ( ( int ) value . Children [ 0 ] . NumberValue ) : 0 ,
value . Children . Count > 1 ? checked ( ( int ) value . Children [ 1 ] . NumberValue ) : 0 ) ;
2026-06-14 05:51:15 +01:00
}
2026-06-14 05:15:18 +01:00
public Shape GetRuntimeShape ( ItemInstance item )
{
var typedItem = GetRuntimeItem ( item ) ;
return ToShape ( typedItem ? . ShapeWidth ? ? 1 , typedItem ? . ShapeHeight ? ? 1 , typedItem ? . ShapeCells ) ;
}
private static Shape ToShape ( int width , int height , IReadOnlyList < AetheriaRuntimeShapeCell > cells )
{
var shape = new Shape ( Math . Max ( width , 1 ) , Math . Max ( height , 1 ) ) ;
if ( cells = = null ) return shape ;
foreach ( var cell in cells )
shape [ new int2 ( cell . X , cell . Y ) ] = true ;
return shape ;
}
2026-06-14 11:40:28 +01:00
public RuntimeItemDefinitionReference CreateReference ( Guid itemId )
2026-06-14 07:27:23 +01:00
{
2026-06-14 11:40:28 +01:00
return new RuntimeItemDefinitionReference ( itemId ) ;
2026-06-14 07:27:23 +01:00
}
2020-04-16 03:08:18 +01:00
public void Log ( string s )
{
_logger ( s ) ;
}
2020-03-22 19:05:26 +00:00
public float GetMass ( ItemInstance item )
{
2026-06-14 04:35:51 +01:00
var typedItem = GetRuntimeItem ( item ) ;
if ( typedItem = = null )
{
return 0 ;
}
2021-04-18 05:13:20 +01:00
return item switch
2020-03-22 19:05:26 +00:00
{
2026-06-14 04:35:51 +01:00
CraftedItemInstance _ = > ( float ) typedItem . Mass ,
SimpleCommodity commodity = > ( float ) typedItem . Mass * commodity . Quantity ,
2021-04-18 05:13:20 +01:00
_ = > 0
} ;
2020-03-22 19:05:26 +00:00
}
2020-06-05 04:16:33 +01:00
public float GetThermalMass ( ItemInstance item )
2020-03-22 19:05:26 +00:00
{
2026-06-14 04:35:51 +01:00
var typedItem = GetRuntimeItem ( item ) ;
if ( typedItem = = null )
{
return 0 ;
}
2021-04-18 05:13:20 +01:00
return item switch
2020-03-22 19:05:26 +00:00
{
2026-06-14 04:35:51 +01:00
CraftedItemInstance _ = > ( float ) ( typedItem . Mass * typedItem . SpecificHeat ) ,
SimpleCommodity commodity = > ( float ) ( typedItem . Mass * typedItem . SpecificHeat * commodity . Quantity ) ,
2021-04-18 05:13:20 +01:00
_ = > 0
} ;
2020-03-22 19:05:26 +00:00
}
// Returns stat when not equipped
2020-12-10 23:20:38 +00:00
public float Evaluate ( PerformanceStat stat , EquippableItem item )
2020-03-22 19:05:26 +00:00
{
2026-06-14 06:23:45 +01:00
var typedItem = GetRuntimeItem ( item ) ;
var maxDurability = ( float ) ( typedItem ? . Durability ? ? 0 ) ;
if ( maxDurability < = 0 )
{
maxDurability = Math . Max ( item . Durability , 1f ) ;
}
2021-03-22 05:22:31 +00:00
var quality = pow ( item . Quality , stat . QualityExponent ) ;
var durabilityExponent = lerp (
GameplaySettings . DurabilityQualityMin ,
GameplaySettings . DurabilityQualityMax ,
pow ( item . Quality , GameplaySettings . DurabilityQualityExponent ) ) ;
2026-06-14 06:23:45 +01:00
var durability = pow ( item . Durability / maxDurability , durabilityExponent * stat . DurabilityExponentMultiplier ) ;
2021-03-22 05:22:31 +00:00
var result = lerp ( stat . Min , stat . Max , quality * durability ) ;
2021-03-27 10:03:24 +00:00
if ( float . IsNaN ( result ) )
2026-06-14 11:12:48 +01:00
throw new InvalidOperationException ( $"Performance Stat on {typedItem?.Name ?? item.ItemId.ToString()} evaluating as NaN: input data is invalid! Durability: {item.Durability} / {maxDurability}" ) ;
2020-03-22 19:05:26 +00:00
return result ;
2021-03-22 05:22:31 +00:00
2020-03-22 19:05:26 +00:00
}
2020-05-25 03:05:30 +01:00
2021-03-20 04:20:17 +00:00
public int GetPrice ( CraftedItemInstance item )
{
2026-06-14 04:35:51 +01:00
var typedItem = GetRuntimeItem ( item ) ;
return typedItem = = null
? 0
: ( int ) ( GameplaySettings . QualityPriceModifier . Evaluate ( item . Quality ) * typedItem . Price ) ;
2021-03-20 04:20:17 +00:00
}
2026-06-14 05:56:41 +01:00
public int GetPrice ( ItemInstance item )
{
var typedItem = GetRuntimeItem ( item ) ;
if ( typedItem = = null )
{
return 0 ;
}
return item switch
{
CraftedItemInstance crafted = > ( int ) ( GameplaySettings . QualityPriceModifier . Evaluate ( crafted . Quality ) * typedItem . Price ) ,
SimpleCommodity commodity = > typedItem . Price * commodity . Quantity ,
_ = > typedItem . Price
} ;
}
2026-06-14 07:41:32 +01:00
public SimpleCommodity CreateSimpleCommodityInstance ( AetheriaRuntimeCatalogItem item , int count )
2020-03-22 19:05:26 +00:00
{
2026-06-14 07:41:32 +01:00
var itemId = GetLegacyGuid ( item ) ;
if ( itemId ! = Guid . Empty )
2020-06-02 07:01:03 +01:00
{
2026-06-14 07:41:32 +01:00
return new SimpleCommodity
2020-03-22 19:05:26 +00:00
{
2026-06-14 11:19:27 +01:00
Reference = CreateReference ( itemId ) ,
2020-05-25 03:05:30 +01:00
Quantity = count
2020-03-22 19:05:26 +00:00
} ;
2020-06-02 07:01:03 +01:00
}
2026-06-14 07:41:32 +01:00
_logger ( "Attempted to create simple commodity instance using missing typed item data" ) ;
2020-04-09 07:48:45 +01:00
return null ;
2020-03-22 19:05:26 +00:00
}
2021-01-19 01:22:56 +00:00
public ItemInstance Instantiate ( ItemInstance item )
{
2026-06-14 07:41:32 +01:00
var typedItem = GetRuntimeItem ( item ) ;
if ( typedItem = = null )
2021-01-27 00:05:30 +00:00
{
2026-06-14 11:12:48 +01:00
_logger ( $"Attempted to instantiate missing item id {item?.ItemId}" ) ;
2026-06-14 07:41:32 +01:00
return null ;
2021-01-27 00:05:30 +00:00
}
2026-06-14 07:41:32 +01:00
ItemInstance instance = item switch
2021-01-27 00:05:30 +00:00
{
2026-06-14 07:41:32 +01:00
SimpleCommodity simple = > CreateSimpleCommodityInstance ( typedItem , simple . Quantity ) ,
CraftedItemInstance = > CreateCraftedInstance ( typedItem ) ,
_ = > null
} ;
if ( instance ! = null )
instance . Rotation = item . Rotation ;
return instance ;
2021-01-19 01:22:56 +00:00
}
2021-03-27 10:03:24 +00:00
2026-06-14 07:41:32 +01:00
public CraftedItemInstance CreateCraftedInstance ( AetheriaRuntimeCatalogItem item , float quality )
2021-03-27 10:03:24 +00:00
{
2026-06-14 07:41:32 +01:00
if ( IsEquippable ( item ) )
return CreateEquippableInstance ( item , quality ) ;
var itemId = GetLegacyGuid ( item ) ;
if ( itemId = = Guid . Empty )
2021-03-27 10:03:24 +00:00
{
2026-06-14 07:41:32 +01:00
throw new NullReferenceException ( "Attempted to create crafted item instance using missing typed item data!" ) ;
}
2026-06-14 09:38:55 +01:00
if ( string . Equals ( item . Category , AetheriaRuntimeItemCategories . Consumable , StringComparison . Ordinal ) )
2026-06-14 07:41:32 +01:00
{
return new ConsumableItem
2021-03-27 10:03:24 +00:00
{
2026-06-14 11:19:27 +01:00
Reference = CreateReference ( itemId ) ,
2026-06-14 07:41:32 +01:00
Quality = quality
2021-03-27 10:03:24 +00:00
} ;
}
2026-06-14 07:41:32 +01:00
return new CompoundCommodity
2021-03-27 10:03:24 +00:00
{
2026-06-14 11:19:27 +01:00
Reference = CreateReference ( itemId ) ,
2021-03-27 10:03:24 +00:00
Quality = quality
} ;
2026-06-14 07:41:32 +01:00
}
public CraftedItemInstance CreateCraftedInstance ( AetheriaRuntimeCatalogItem item )
{
return CreateCraftedInstance ( item , SelectCraftedQuality ( ) ) ;
2021-03-27 10:03:24 +00:00
}
2026-06-14 06:35:34 +01:00
2026-06-14 07:27:23 +01:00
public EquippableItem CreateEquippableInstance ( AetheriaRuntimeCatalogItem item , float quality )
{
var itemId = GetLegacyGuid ( item ) ;
if ( itemId = = Guid . Empty )
{
throw new NullReferenceException ( "Attempted to create equippable item instance using missing typed item data!" ) ;
}
return new EquippableItem
{
2026-06-14 11:19:27 +01:00
Reference = CreateReference ( itemId ) ,
2026-06-14 07:27:23 +01:00
Quality = quality ,
Durability = item . Durability > 0 ? ( float ) item . Durability : 1f
} ;
}
public EquippableItem CreateEquippableInstance ( AetheriaRuntimeCatalogItem item )
{
return CreateEquippableInstance ( item , SelectCraftedQuality ( ) ) ;
}
private float SelectCraftedQuality ( )
{
2021-03-08 02:57:38 +00:00
var quality = Random . NextFloat ( ) ;
var tier = GameplaySettings . Tiers [ 0 ] ;
foreach ( var t in GameplaySettings . Tiers )
{
if ( t . Rarity > quality )
tier = t ;
}
2020-03-22 19:05:26 +00:00
2026-06-14 07:27:23 +01:00
return tier . Quality ;
}
private static Guid GetLegacyGuid ( AetheriaRuntimeCatalogItem item )
{
return item ! = null & & Guid . TryParse ( item . LegacyId , out var itemId )
? itemId
: Guid . Empty ;
2020-03-22 19:05:26 +00:00
}
2020-05-25 03:05:30 +01:00
2026-06-14 07:41:32 +01:00
private static bool IsEquippable ( AetheriaRuntimeCatalogItem item )
{
return ! string . IsNullOrWhiteSpace ( item ? . HardpointType ) | |
2026-06-14 09:38:55 +01:00
string . Equals ( item ? . Category , AetheriaRuntimeItemCategories . Hull , StringComparison . Ordinal ) ;
2026-06-14 07:41:32 +01:00
}
2021-03-08 02:57:38 +00:00
public ( RarityTier tier , int upgrades ) GetTier ( CraftedItemInstance item )
{
var tier = GameplaySettings . Tiers [ 0 ] ;
foreach ( var t in GameplaySettings . Tiers )
if ( item . Quality + . 001f > t . Quality )
tier = t ;
int upgrades = ( int ) ( ( item . Quality - tier . Quality ) / . 0499f ) ;
return ( tier , upgrades ) ;
}
2026-06-13 07:11:16 +01:00
}