Aetheria/Assets/Scripts/ServerShared/ItemManager.cs
Meta Vens aaab561f98 Consumable host: initialise at activation, dispose at expiry, instantiate consumables
ActivateConsumable runs Initialize on the effect's behaviours; expiry disposes
them before Resolver.Forget, so a consumable's modifiers exist exactly while
its effect is active. ItemManager.CreateInstance returns a ConsumableItem for a
consumable design. StatResolver.DetachModifier drops an entry left with
nothing attached, so ModifierEntryCount returns to baseline. LoadoutTests'
hand-built ConsumableItem goes through CreateInstance. Builds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 20:19:22 +02:00

241 lines
9 KiB
C#

/* 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;
using System.Collections.Generic;
using System.Linq;
using GameCult.Caching;
using CultMath;
using static CultMath.math;
using float2 = CultMath.float2;
using Random = CultMath.Random;
using JM.LinqFaster;
using UniRx;
using float4 = CultMath.float4;
public class ItemManager
{
public Random Random = new Random((uint) (DateTime.Now.Ticks%uint.MaxValue));
private Action<string> _logger;
public CultCache ItemData { get; }
public ProvenanceLedger Lots { get; }
public GameplaySettings GameplaySettings { get; }
public ItemManager(CultCache itemData, ProvenanceLedger lots, GameplaySettings settings, Action<string> logger)
{
ItemData = itemData;
Lots = lots;
GameplaySettings = settings;
_logger = logger;
}
public void Log(string s)
{
_logger(s);
}
// The one resolution path from an item instance to its design
public ItemData GetData(ItemInstance item) => ItemData.Get(item.Data);
public SimpleCommodityData GetData(SimpleCommodity item) => GetData((ItemInstance) item) as SimpleCommodityData;
public CraftedItemData GetData(CraftedItemInstance item) => GetData((ItemInstance) item) as CraftedItemData;
public EquippableItemData GetData(EquippableItem item) => GetData((ItemInstance) item) as EquippableItemData;
public float GetMass(ItemInstance item)
{
return item switch
{
CraftedItemInstance _ => GetData(item).Mass,
SimpleCommodity commodity => GetData(item).Mass * commodity.Quantity,
_ => 0
};
}
public float GetThermalMass(ItemInstance item)
{
var data = GetData(item);
return item switch
{
CraftedItemInstance _ => data.Mass * data.SpecificHeat,
SimpleCommodity commodity => data.Mass * data.SpecificHeat * commodity.Quantity,
_ => 0
};
}
// The one resolution path from a crafted instance to the lot it was minted from.
public Lot GetLot(CraftedItemInstance item) => Lots[item.Lot];
// Returns stat when not equipped: a null-entity context with no heat, no power and no modifiers. It says so
// in its own name (UnequippedStatContext) rather than being a third quiet dialect of the arithmetic.
public float Evaluate(PerformanceStat stat, EquippableItem item)
{
var data = GetData(item);
var result = stat.Evaluate(new UnequippedStatContext(this, item, data));
if (float.IsNaN(result))
throw new InvalidOperationException($"Performance Stat on {data.Name} evaluating as NaN: input data is invalid! Durability: {item.Durability} / {data.Durability}");
return result;
}
// The unequipped case: quality and durability read live off the item and its lot; heat and modifiers do not
// exist without an entity, so they resolve as the identity for their term rather than a bespoke formula.
private sealed class UnequippedStatContext : IStatContext
{
private readonly EquippableItem _item;
private readonly EquippableItemData _data;
private readonly float _durabilityExponent;
public Lot Lot { get; }
public UnequippedStatContext(ItemManager items, EquippableItem item, EquippableItemData data)
{
_item = item;
_data = data;
Lot = items.GetLot(item);
_durabilityExponent = lerp(
items.GameplaySettings.DurabilityQualityMin,
items.GameplaySettings.DurabilityQualityMax,
pow(Lot.Quality, items.GameplaySettings.DurabilityQualityExponent));
}
public float HeatFactor(float exponent) => 1f;
public float DurabilityFactor(float exponent) =>
pow(_item.Durability / _data.Durability, _durabilityExponent * exponent);
public float ConsumableProgressFactor(float exponent) => 1f;
public float PowerSupplyFactor(float exponent) => 1f;
public float ScaleModifier(PerformanceStat stat) => 1f;
public float ConstantModifier(PerformanceStat stat) => 0f;
}
public int GetPrice(CraftedItemInstance item)
{
var data = GetData(item);
return (int) (GameplaySettings.QualityPriceModifier.Evaluate(GetLot(item).Quality) * data.Price);
}
// The tier roll every mint applies, whether or not the lot fills roles.
public float RollQuality()
{
var quality = Random.NextFloat();
var tier = GameplaySettings.Tiers[0];
foreach (var t in GameplaySettings.Tiers)
{
if (t.Rarity > quality)
tier = t;
}
return tier.Quality;
}
// Builds one of a manufacturer's branded products: the unit's own workmanship rolled as before, and each of
// the design's roles filled with a part whose quality is drawn from that manufacturer's distribution for it.
public int CreateLot(FactionProductData product)
{
var design = ItemData.Get(product.Design);
var lot = new Lot
{
Design = ItemData.RefOf<ItemData>(design),
Origin = new Attributed { Faction = product.Manufacturer },
Product = ItemData.RefOf(product),
Quality = RollQuality(),
Roles = new List<RoleFill>()
};
if (design.Roles != null)
foreach (var role in design.Roles)
{
var build = product.Roles?.FirstOrDefault(b => b.Role == role.Name) ?? new ProductRole();
lot.Roles.Add(new RoleFill
{
Role = role.Name,
Quality = clamp(Random.NextGaussian(build.Mean, build.StandardDeviation), .01f, 1)
});
}
return Lots.Add(lot);
}
// No branded product: a lot attributed to maker (which may be unset) with no role fills, e.g. console `give`.
public int CreateLot(CraftedItemData design, CultRecordRef<Faction> maker, float quality)
{
var lot = new Lot
{
Design = ItemData.RefOf<ItemData>(design),
Origin = new Attributed { Faction = maker },
Quality = quality
};
return Lots.Add(lot);
}
// The one writer of a crafted instance's Data: copied from the lot's design, never independently set.
public CraftedItemInstance CreateInstance(int lot)
{
var l = Lots[lot];
var design = ItemData.Get(l.Design) as CraftedItemData;
if (design is EquippableItemData equippableItemData)
{
return new EquippableItem
{
Data = l.Design, Lot = lot, Durability = equippableItemData.Durability
};
}
if (design is ConsumableItemData)
return new ConsumableItem { Data = l.Design, Lot = lot };
return new CompoundCommodity
{
Data = l.Design, Lot = lot
};
}
public CraftedItemInstance CreateInstance(FactionProductData product)
{
var design = ItemData.Get(product.Design);
if (design == null)
{
_logger($"Product {product.Name} names a design that does not exist!");
return null;
}
return CreateInstance(CreateLot(product));
}
// Who made an item and what it is branded as. The maker derives from its lot's provenance and the product is the
// one its lot records; nothing re-derives it. Null maker (an unset faction, or an Extracted lot with none) means
// no brand at all; a set maker whose lot records no product (or one the catalog no longer holds) still shows
// the maker, with no product name or flavour text.
public (Faction Maker, FactionProductData Product) Brand(CraftedItemInstance item)
{
var lot = GetLot(item);
var maker = lot.Origin switch
{
Attributed attributed => attributed.Faction,
Produced produced => produced.Faction,
_ => default
};
if (!maker.IsSet()) return (null, null);
var product = ItemData.Get(lot.Product);
return (ItemData.Get(maker), product);
}
// What the player reads an item as: its lot's product name when it records one,
// otherwise the design's own name (unbranded, console-given and extracted lots, and commodities).
public string Title(ItemInstance item) =>
(item is CraftedItemInstance crafted ? Brand(crafted).Product?.Name : null) ?? GetData(item).Name;
public (RarityTier tier, int upgrades) GetTier(CraftedItemInstance item)
{
var quality = GetLot(item).Quality;
var tier = GameplaySettings.Tiers[0];
foreach (var t in GameplaySettings.Tiers)
if (quality + .001f > t.Quality)
tier = t;
int upgrades = (int) ((quality - tier.Quality) / .0499f);
return (tier, upgrades);
}
}