156 lines
8.5 KiB
C#
156 lines
8.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
|
|
// What a station pays for goods and charges to mend gear. The one owner of the sell price, the repair cost, and the
|
|
// two commits that move goods or restore durability against the run's credits; TradeMenu shows these numbers and
|
|
// decides none of them. It holds no Unity type. Credits are the caller's: a refusal leaves them, the cargo and the
|
|
// durabilities exactly as they were.
|
|
public static class StationServices
|
|
{
|
|
// What the station charges for the item, or for `quantity` units of a commodity (a crafted item is one unit). The
|
|
// number TradeMenu shows, checks against the run's credits and charges.
|
|
public static int BuyPrice(ItemManager items, ItemInstance item, int quantity = 1)
|
|
{
|
|
switch (item)
|
|
{
|
|
case CraftedItemInstance crafted:
|
|
return items.GetPrice(crafted);
|
|
case SimpleCommodity commodity:
|
|
return (int) Math.Min((long) items.GetData(commodity).Price * quantity, int.MaxValue);
|
|
default:
|
|
throw new ArgumentException($"{item?.GetType().Name ?? "null"} has no buy price.", nameof(item));
|
|
}
|
|
}
|
|
|
|
// What the station pays for the item: its price, scaled by SellFraction and by its condition. A commodity has no
|
|
// wear, so it sells at the fraction alone; a simple commodity's price is per unit.
|
|
public static int SellPrice(ItemManager items, ItemInstance item)
|
|
{
|
|
var fraction = (double) items.GameplaySettings.SellFraction;
|
|
switch (item)
|
|
{
|
|
case CraftedItemInstance crafted:
|
|
return SellValue(items.GetPrice(crafted), fraction, Condition(items, crafted));
|
|
case SimpleCommodity commodity:
|
|
return (int) Math.Floor((long) items.GetData(commodity).Price * commodity.Quantity * fraction);
|
|
default:
|
|
throw new ArgumentException($"{item?.GetType().Name ?? "null"} has no sell price.", nameof(item));
|
|
}
|
|
}
|
|
|
|
// What the station charges to restore the entity's hull armour, hull and every item it carries to design: each
|
|
// piece's share of its price in proportion to its wear, scaled by the repair fraction, rounded up. Armour has no
|
|
// price of its own, so the worn share of the hull's total armour is charged as that share of the hull's price.
|
|
public static int RepairCost(ItemManager items, Entity entity)
|
|
{
|
|
var fraction = RepairFraction(items);
|
|
long total = (long) Math.Ceiling(ArmourWear(entity) * items.GetPrice(entity.Hull) * fraction);
|
|
foreach (var gear in Gear(entity)) total += RepairShare(items, gear, fraction);
|
|
return (int) Math.Min(total, int.MaxValue);
|
|
}
|
|
|
|
// What the station charges for each unit of wear it mends: RepairFraction, but never less than SellFraction, so
|
|
// mending an item before selling it never pays more than the repair cost.
|
|
private static double RepairFraction(ItemManager items) =>
|
|
Math.Max(items.GameplaySettings.RepairFraction, items.GameplaySettings.SellFraction);
|
|
|
|
// The worn share of the entity's armour, 0 when it is whole or has none.
|
|
private static double ArmourWear(Entity entity)
|
|
{
|
|
double max = 0, worn = 0;
|
|
for (var x = 0; x < entity.MaxArmor.GetLength(0); x++)
|
|
for (var y = 0; y < entity.MaxArmor.GetLength(1); y++)
|
|
{
|
|
max += entity.MaxArmor[x, y];
|
|
worn += Math.Max(0, entity.MaxArmor[x, y] - entity.Armor[x, y]);
|
|
}
|
|
return max > 0 ? worn / max : 0;
|
|
}
|
|
|
|
// The purchase: the station charges BuyPrice, and `deliver` puts the goods where they go. Credits are checked
|
|
// and charged here, at the same boundary as TryRepair: a price equal to the credits held is affordable.
|
|
public static BuyResult TryBuy(ItemManager items, ItemInstance item, int quantity, ref int credits, Func<bool> deliver)
|
|
{
|
|
var price = BuyPrice(items, item, quantity);
|
|
if (price > credits) return BuyResult.ShortOfCredits;
|
|
if (!deliver()) return BuyResult.NoRoom;
|
|
credits -= price;
|
|
return BuyResult.Bought;
|
|
}
|
|
|
|
// The purchase of `quantity` units of a station stack, delivered as one lot. A bay holds no stack above its
|
|
// design's MaxStack, so a stack the station holds is a lot already; a stack above it comes from outside a bay and is
|
|
// refused, as is a purchase of more than the stack holds or of a stack that holds nothing. All of that is refused
|
|
// before anything moves, so `deliver` moves units the station has and the charge is for the units it delivered.
|
|
public static BuyResult TryBuyLots(ItemManager items, SimpleCommodity stock, int quantity, ref int credits, Func<int, bool> deliver)
|
|
{
|
|
if (quantity < 1 || quantity > stock.Quantity || stock.Quantity > items.GetData(stock).MaxStack) return BuyResult.NoRoom;
|
|
return TryBuy(items, stock, quantity, ref credits, () => deliver(quantity));
|
|
}
|
|
|
|
// The quantity a Buy Quantity field may hold: what was typed, at most what the credits cover and what the station
|
|
// has, and never below one unit (a price the credits do not cover is then refused as short of credits).
|
|
public static int BuyableQuantity(ItemManager items, SimpleCommodity stock, int typed, int credits) =>
|
|
Math.Max(1, Math.Min(Math.Min(typed, credits / BuyPrice(items, stock)), stock.Quantity));
|
|
|
|
// The sale: the item leaves the bay for one of the station's cargo bays and the station pays SellPrice. False,
|
|
// with the item in its bay and the credits unchanged, when no station bay can take it.
|
|
public static bool TrySell(EquippedCargoBay from, Entity station, ItemInstance item, ref int credits)
|
|
{
|
|
var price = SellPrice(station.ItemManager, item);
|
|
var moved = item is SimpleCommodity commodity
|
|
? station.CargoBays.Any(bay => from.TryTransferItem(bay, commodity, commodity.Quantity))
|
|
: station.CargoBays.Any(bay => from.TryTransferItem(bay, (CraftedItemInstance) item));
|
|
if (!moved) return false;
|
|
credits += price;
|
|
return true;
|
|
}
|
|
|
|
// The repair: armour and every durability go back to design and the station charges RepairCost. False, with
|
|
// nothing restored, when the credits fall short.
|
|
public static bool TryRepair(Entity entity, ref int credits)
|
|
{
|
|
var items = entity.ItemManager;
|
|
var cost = RepairCost(items, entity);
|
|
if (cost > credits) return false;
|
|
foreach (var gear in Gear(entity))
|
|
if (items.GetData(gear) is EquippableItemData design && design.Durability > 0)
|
|
gear.Durability = design.Durability;
|
|
Array.Copy(entity.MaxArmor, entity.Armor, entity.MaxArmor.Length);
|
|
credits -= cost;
|
|
return true;
|
|
}
|
|
|
|
// Equipment holds the hull itself, so the hull is in the sum once.
|
|
private static IEnumerable<EquippableItem> Gear(Entity entity) =>
|
|
entity.Equipment.Select(equipped => equipped.EquippableItem)
|
|
.Concat(entity.CargoBays.Select(bay => bay.EquippableItem))
|
|
.Concat(entity.DockingBays.Select(bay => bay.EquippableItem));
|
|
|
|
// Instance durability over the design's, for gear that wears; everything else is as good as new.
|
|
private static double Condition(ItemManager items, CraftedItemInstance item)
|
|
{
|
|
if (!(item is EquippableItem gear) || !(items.GetData(gear) is EquippableItemData design) || design.Durability <= 0)
|
|
return 1;
|
|
return Math.Max(0, Math.Min(1, gear.Durability / (double) design.Durability));
|
|
}
|
|
|
|
// The sell value of a price at a condition, rounded down. The one place the sell rounding lives: RepairShare
|
|
// prices the sell value a repair adds from the same function, so the two cannot round apart.
|
|
private static int SellValue(int price, double fraction, double condition) =>
|
|
(int) Math.Floor(price * fraction * condition);
|
|
|
|
// The share of the repair charge for one piece of gear: its wear at the repair fraction, rounded up, and never
|
|
// less than the sell value the repair adds, so mending before selling never nets credits.
|
|
private static long RepairShare(ItemManager items, EquippableItem gear, double fraction)
|
|
{
|
|
var price = items.GetPrice(gear);
|
|
var condition = Condition(items, gear);
|
|
var sellFraction = (double) items.GameplaySettings.SellFraction;
|
|
var added = SellValue(price, sellFraction, 1) - SellValue(price, sellFraction, condition);
|
|
return Math.Max(added, (long) Math.Ceiling((1 - condition) * price * fraction));
|
|
}
|
|
}
|
|
|
|
public enum BuyResult { Bought, ShortOfCredits, NoRoom }
|