Aetheria/Assets/Scripts/ServerShared/ItemManager.cs

380 lines
14 KiB
C#
Raw Normal View History

2026-06-13 17:22:03 +01:00
/* This Source Code Form is subject to the terms of the Mozilla Public
* 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;
using System.Collections.Generic;
using System.Linq;
2026-06-21 09:54:15 +01:00
using GameCult.Aetheria.State.Verse;
2026-06-21 09:37:54 +01:00
using Random = CultMath.Random;
using JM.LinqFaster;
using UniRx;
public class ItemManager
{
public Random Random = new Random((uint) (DateTime.Now.Ticks%uint.MaxValue));
2026-06-16 13:11:38 +01:00
private Action<string> _logger;
private readonly AetheriaRuntimeCatalogSnapshot _runtimeCatalog;
public GameplaySettings GameplaySettings { get; }
public ItemManager(AetheriaRuntimeCatalogSnapshot runtimeCatalog, GameplaySettings settings, Action<string> logger)
{
_runtimeCatalog = runtimeCatalog ?? throw new ArgumentNullException(nameof(runtimeCatalog));
GameplaySettings = settings;
_logger = logger;
}
2026-06-14 04:35:51 +01:00
public AetheriaRuntimeCatalogItem GetRuntimeItem(ItemInstance item)
{
2026-06-28 12:21:38 +01:00
return _runtimeCatalog.FindItem(item, x => x.ItemKey);
2026-06-14 04:35:51 +01:00
}
public Behavior[] CreateRuntimeBehaviors(EquippedItem item)
{
return GetRuntimeItem(item?.EquippableItem)?.BehaviorPayloads
.Select(payload => CreateRuntimeBehavior(payload, item))
.Where(behavior => behavior != null)
.ToArray() ?? Array.Empty<Behavior>();
}
public Behavior[] CreateRuntimeBehaviors(ConsumableItemEffect effect)
{
return GetRuntimeItem(effect?.Item)?.BehaviorPayloads
.Select(payload => CreateRuntimeBehavior(payload, effect))
.Where(behavior => behavior != null)
.ToArray() ?? Array.Empty<Behavior>();
}
private static Behavior CreateRuntimeBehavior(AetheriaRuntimeBehaviorPayload payload, EquippedItem item)
{
2026-06-15 20:12:34 +01:00
return CreateDirectRuntimeBehavior(payload, item);
}
private static Behavior CreateRuntimeBehavior(AetheriaRuntimeBehaviorPayload payload, ConsumableItemEffect effect)
{
2026-06-15 20:12:34 +01:00
return CreateDirectRuntimeBehavior(payload, effect);
}
private static Behavior CreateDirectRuntimeBehavior(AetheriaRuntimeBehaviorPayload payload, EquippedItem item)
{
var definition = new RuntimeBehaviorDefinition(payload);
switch (payload.Kind)
{
case "Cockpit": return new Cockpit(definition, item);
2026-06-15 19:54:54 +01:00
case "AetherDrive": return new AetherDrive(definition, item);
2026-06-15 20:12:34 +01:00
case "AutoWeapon": return new AutoWeapon(definition, item);
2026-06-15 19:20:06 +01:00
case "Capacitor": return new Capacitor(definition, item);
2026-06-15 20:12:34 +01:00
case "ChargedWeapon": return new ChargedWeapon(definition, item);
2026-06-15 19:06:13 +01:00
case "Cooldown": return new Cooldown(definition, item);
2026-06-15 20:12:34 +01:00
case "ConstantWeapon": return new ConstantWeapon(definition, item);
2026-06-15 19:06:13 +01:00
case "EnergyDraw": return new EnergyDraw(definition, item);
2026-06-15 20:12:34 +01:00
case "GuidedWeapon": return new InstantWeapon(definition, item);
2026-06-15 19:06:13 +01:00
case "Heat": return new Heat(definition, item);
case "HeatStorage": return new HeatStorage(definition, item);
2026-06-15 20:12:34 +01:00
case "InstantWeapon": return new InstantWeapon(definition, item);
2026-06-15 19:06:13 +01:00
case "ItemUsage": return new ItemUsage(definition, item);
2026-06-15 20:12:34 +01:00
case "Launcher": return new LockWeapon(definition, item);
case "LockWeapon": return new LockWeapon(definition, item);
2026-06-15 19:20:06 +01:00
case "MiningTool": return new MiningTool(definition, item);
2026-06-15 19:29:34 +01:00
case "Radiator": return new Radiator(definition, item);
case "Reactor": return new Reactor(definition, item);
2026-06-15 19:06:13 +01:00
case "Reflector": return new Reflector(definition, item);
2026-06-15 19:20:06 +01:00
case "ResourceScanner": return new ResourceScanner(definition, item);
2026-06-15 19:29:34 +01:00
case "Sensor": return new Sensor(definition, item);
2026-06-15 19:20:06 +01:00
case "Shield": return new Shield(definition, item);
2026-06-15 19:29:34 +01:00
case "StatModifier": return new StatModifier(definition, item);
case "Switch": return new Switch(definition, item);
2026-06-15 19:20:06 +01:00
case "Thermotoggle": return new Thermotoggle(definition, item);
case "Thruster": return new Thruster(definition, item);
case "Trigger": return new Trigger(definition, item);
case "TurretController": return new TurretController(definition, item);
2026-06-15 19:06:13 +01:00
case "VelocityConversion": return new VelocityConversion(definition, item);
case "VelocityLimit": return new VelocityLimit(definition, item);
case "Visibility": return new Visibility(definition, item);
case "Wear": return new Wear(definition, item);
default: return null;
}
}
private static Behavior CreateDirectRuntimeBehavior(AetheriaRuntimeBehaviorPayload payload, ConsumableItemEffect effect)
{
var definition = new RuntimeBehaviorDefinition(payload);
switch (payload.Kind)
{
case "Cockpit": return new Cockpit(definition, effect);
2026-06-15 19:54:54 +01:00
case "AetherDrive": return new AetherDrive(definition, effect);
2026-06-15 20:12:34 +01:00
case "AutoWeapon": return new InstantWeapon(definition, effect);
2026-06-15 19:20:06 +01:00
case "Capacitor": return new Capacitor(definition, effect);
2026-06-15 20:12:34 +01:00
case "ChargedWeapon": return new ChargedWeapon(definition, effect);
2026-06-15 19:06:13 +01:00
case "Cooldown": return new Cooldown(definition, effect);
2026-06-15 20:12:34 +01:00
case "ConstantWeapon": return new ConstantWeapon(definition, effect);
2026-06-15 19:06:13 +01:00
case "EnergyDraw": return new EnergyDraw(definition, effect);
2026-06-15 20:12:34 +01:00
case "GuidedWeapon": return new InstantWeapon(definition, effect);
2026-06-15 19:06:13 +01:00
case "Heat": return new Heat(definition, effect);
case "HeatStorage": return new HeatStorage(definition, effect);
2026-06-15 20:12:34 +01:00
case "InstantWeapon": return new InstantWeapon(definition, effect);
2026-06-15 19:06:13 +01:00
case "ItemUsage": return new ItemUsage(definition, effect);
2026-06-15 20:12:34 +01:00
case "Launcher": return new LockWeapon(definition, effect);
case "LockWeapon": return new LockWeapon(definition, effect);
2026-06-15 19:20:06 +01:00
case "MiningTool": return new MiningTool(definition, effect);
2026-06-15 19:29:34 +01:00
case "Radiator": return new Radiator(definition, effect);
case "Reactor": return new Reactor(definition, effect);
2026-06-15 19:06:13 +01:00
case "Reflector": return new Reflector(definition, effect);
2026-06-15 19:20:06 +01:00
case "ResourceScanner": return new ResourceScanner(definition, effect);
2026-06-15 19:29:34 +01:00
case "Sensor": return new Sensor(definition, effect);
2026-06-15 19:20:06 +01:00
case "Shield": return new Shield(definition, effect);
2026-06-15 19:29:34 +01:00
case "StatModifier": return new StatModifier(definition, effect);
case "Switch": return new Switch(definition, effect);
2026-06-15 19:20:06 +01:00
case "Thermotoggle": return new Thermotoggle(definition, effect);
case "Thruster": return new Thruster(definition, effect);
case "Trigger": return new Trigger(definition, effect);
case "TurretController": return new TurretController(definition, effect);
2026-06-15 19:06:13 +01:00
case "VelocityConversion": return new VelocityConversion(definition, effect);
case "VelocityLimit": return new VelocityLimit(definition, effect);
case "Visibility": return new Visibility(definition, effect);
case "Wear": return new Wear(definition, effect);
default: return null;
}
}
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)
2026-06-21 09:37:54 +01:00
shape.SetCell(cell.X, cell.Y, true);
return shape;
}
public AetheriaRuntimeItemReference CreateReference(AetheriaRuntimeCatalogItem item)
{
return new AetheriaRuntimeItemReference(ToItemKey(item));
}
private static string ToItemKey(AetheriaRuntimeCatalogItem item)
{
2026-06-15 01:31:16 +01:00
return item?.ItemKey ?? "";
}
public void Log(string s)
{
_logger(s);
}
public float GetMass(ItemInstance item)
{
2026-06-14 04:35:51 +01:00
var typedItem = GetRuntimeItem(item);
if (typedItem == null)
{
return 0;
}
return item switch
{
2026-06-14 04:35:51 +01:00
CraftedItemInstance _ => (float)typedItem.Mass,
SimpleCommodity commodity => (float)typedItem.Mass * commodity.Quantity,
_ => 0
};
}
public float GetThermalMass(ItemInstance item)
{
2026-06-14 04:35:51 +01:00
var typedItem = GetRuntimeItem(item);
if (typedItem == null)
{
return 0;
}
return item switch
{
2026-06-14 04:35:51 +01:00
CraftedItemInstance _ => (float)(typedItem.Mass * typedItem.SpecificHeat),
SimpleCommodity commodity => (float)(typedItem.Mass * typedItem.SpecificHeat * commodity.Quantity),
_ => 0
};
}
// Returns stat when not equipped
public float Evaluate(PerformanceStat stat, EquippableItem item)
{
var typedItem = GetRuntimeItem(item);
var maxDurability = (float)(typedItem?.Durability ?? 0);
if (maxDurability <= 0)
{
maxDurability = Math.Max(item.Durability, 1f);
}
2026-06-21 09:37:54 +01:00
var durabilityRatio = item.Durability / maxDurability;
var durabilityExponent = Lerp(
GameplaySettings.DurabilityQualityMin,
GameplaySettings.DurabilityQualityMax,
2026-06-21 09:37:54 +01:00
MathF.Pow(item.Quality, GameplaySettings.DurabilityQualityExponent));
var result = stat.EvaluateRecipeOrLegacy(
new StatEvaluationContext(
quality: item.Quality,
durability: durabilityRatio,
heat: 1),
durabilityExponent,
thermalExponent: 0,
includeHeat: false);
if (float.IsNaN(result))
throw new InvalidOperationException($"Performance Stat on {typedItem?.Name ?? item.ItemKey} evaluating as NaN: input data is invalid! Durability: {item.Durability} / {maxDurability}");
return result;
}
2026-06-21 09:37:54 +01:00
private static float Lerp(float from, float to, float t)
{
return from + (to - from) * t;
}
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);
}
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)
{
2026-06-15 01:31:16 +01:00
if (!string.IsNullOrWhiteSpace(item?.ItemKey))
{
2026-06-14 07:41:32 +01:00
return new SimpleCommodity
{
Reference = CreateReference(item),
Quantity = count
};
}
2026-06-14 07:41:32 +01:00
_logger("Attempted to create simple commodity instance using missing typed item data");
return null;
}
public ItemInstance Instantiate(ItemInstance item)
{
2026-06-14 07:41:32 +01:00
var typedItem = GetRuntimeItem(item);
if (typedItem == null)
{
_logger($"Attempted to instantiate missing item key {item?.ItemKey}");
2026-06-14 07:41:32 +01:00
return null;
}
2026-06-14 07:41:32 +01:00
ItemInstance instance = item switch
{
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;
}
2026-06-14 07:41:32 +01:00
public CraftedItemInstance CreateCraftedInstance(AetheriaRuntimeCatalogItem item, float quality)
{
2026-06-14 07:41:32 +01:00
if (IsEquippable(item))
return CreateEquippableInstance(item, quality);
2026-06-15 01:31:16 +01:00
if (string.IsNullOrWhiteSpace(item?.ItemKey))
{
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
{
Reference = CreateReference(item),
2026-06-14 07:41:32 +01:00
Quality = quality
};
}
2026-06-14 07:41:32 +01:00
return new CompoundCommodity
{
Reference = CreateReference(item),
Quality = quality
};
2026-06-14 07:41:32 +01:00
}
public CraftedItemInstance CreateCraftedInstance(AetheriaRuntimeCatalogItem item)
{
return CreateCraftedInstance(item, SelectCraftedQuality());
}
public EquippableItem CreateEquippableInstance(AetheriaRuntimeCatalogItem item, float quality)
{
2026-06-15 01:31:16 +01:00
if (string.IsNullOrWhiteSpace(item?.ItemKey))
{
throw new NullReferenceException("Attempted to create equippable item instance using missing typed item data!");
}
return new EquippableItem
{
Reference = CreateReference(item),
Quality = quality,
Durability = item.Durability > 0 ? (float)item.Durability : 1f
};
}
public EquippableItem CreateEquippableInstance(AetheriaRuntimeCatalogItem item)
{
return CreateEquippableInstance(item, SelectCraftedQuality());
}
private float SelectCraftedQuality()
{
var quality = Random.NextFloat();
var tier = GameplaySettings.Tiers[0];
foreach (var t in GameplaySettings.Tiers)
{
if (t.Rarity > quality)
tier = t;
}
return tier.Quality;
}
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
}
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
}