Trading mod: shops as data and the trade window

This commit is contained in:
cthangover committed 2026-10-23 09:30:05 +03:00
commit 89ee23f1e6
19 files changed
+1595

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
namespace Cthangover.Mods.Trading.Tests
{
using System.Collections.Generic;
using System.Text.Json;
using Cthangover.Core.Mods.Testing;
/// <summary>A shop is hand-written JSON: it must survive a round trip and its lookups must skip broken lines.</summary>
public class ShopInfoTests
{
[ModTest("shop/json_round_trip")]
static void JsonRoundTrip(ModTestContext ctx)
{
var original = new ShopInfo
{
ID = "rpg_demo/village_merchant",
Name = "rpg_demo/shop/village_merchant",
Currency = "money/shell",
Sells = new List<ShopOffer> { new() { Item = "rpg_demo/elixir", Price = 10 } },
Buys = new List<ShopOffer> { new() { Item = "rpg_demo/stones", Price = 2 } },
};
var restored = JsonSerializer.Deserialize<ShopInfo>(JsonSerializer.Serialize(original));
ModAssert.Equal(original.ID, restored.ID, "Id");
ModAssert.Equal(original.Name, restored.Name, "Name");
ModAssert.Equal(original.Currency, restored.Currency, "Currency");
ModAssert.Equal(1, restored.Sells.Count, "Sells");
ModAssert.Equal("rpg_demo/elixir", restored.Sells[0].Item, "Sells item");
ModAssert.Equal(10, restored.Sells[0].Price, "Sells price");
ModAssert.Equal(1, restored.Buys.Count, "Buys");
ModAssert.Equal("rpg_demo/stones", restored.Buys[0].Item, "Buys item");
ModAssert.Equal(2, restored.Buys[0].Price, "Buys price");
}
[ModTest("shop/defaults_from_minimal_json")]
static void DefaultsFromMinimalJson(ModTestContext ctx)
{
var shop = JsonSerializer.Deserialize<ShopInfo>("{\"Id\":\"bare\"}");
ModAssert.Equal("bare", shop.ID, "Id");
ModAssert.Equal(ShopInfo.DefaultCurrency, shop.Currency, "default currency");
ModAssert.Equal(0, shop.Sells.Count, "no goods");
ModAssert.Null(shop.FindBuy("anything"), "buys nothing");
}
[ModTest("shop/find_skips_broken_offers")]
static void FindSkipsBrokenOffers(ModTestContext ctx)
{
var shop = new ShopInfo
{
ID = "s",
Sells = new List<ShopOffer> { null, new() { Item = "a", Price = 0 }, new() { Item = "a", Price = 4 } },
Buys = null,
};
ModAssert.Equal(4, shop.FindSell("a")?.Price ?? -1, "the first offer with a real price wins");
ModAssert.Null(shop.FindSell("b"), "unknown item");
ModAssert.Null(shop.FindSell(null), "null id");
ModAssert.Null(shop.FindBuy("a"), "null list buys nothing");
}
}
}
+107
View File
@@ -0,0 +1,107 @@
namespace Cthangover.Mods.Trading.Tests
{
using System.Collections.Generic;
using System.Linq;
using Cthangover.Core.Items;
using Cthangover.Core.Mods.Resolvers;
using Cthangover.Core.Mods.Testing;
/// <summary>
/// Deals are all or nothing: a refused buy or sell must leave both the money
/// and the goods exactly as they were.
/// </summary>
public class TradeLedgerTests
{
private static string Goods() =>
Entities.Collect<ItemInfo>().Keys.First(k => k != ShopInfo.DefaultCurrency);
private static ShopInfo Shop(string goods) => new()
{
ID = "test_shop",
Sells = new List<ShopOffer> { new() { Item = goods, Price = 3 } },
Buys = new List<ShopOffer> { new() { Item = goods, Price = 2 } },
};
[ModTest("ledger/buy_takes_money_and_adds_goods")]
static void BuyTakesMoneyAndAddsGoods(ModTestContext ctx)
{
var goods = Goods();
var shop = Shop(goods);
var inventory = new Inventory();
inventory.Add(shop.Currency, 10);
var before = inventory.CheckCount(goods);
ModAssert.True(TradeLedger.Buy(inventory, shop, goods, 3), "affordable buy succeeds");
ModAssert.Equal(1, inventory.CheckCount(shop.Currency), "10 - 3 x 3 coins left");
ModAssert.Equal(before + 3, inventory.CheckCount(goods), "three units bought");
ModAssert.Equal(1, TradeLedger.Money(inventory, shop), "money reads the currency stack");
}
[ModTest("ledger/buy_refused_changes_nothing")]
static void BuyRefusedChangesNothing(ModTestContext ctx)
{
var goods = Goods();
var shop = Shop(goods);
var inventory = new Inventory();
inventory.Add(shop.Currency, 5);
var before = inventory.CheckCount(goods);
ModAssert.False(TradeLedger.Buy(inventory, shop, goods, 2), "6 coins needed, 5 owned");
ModAssert.False(TradeLedger.Buy(inventory, shop, goods, 0), "zero units");
ModAssert.False(TradeLedger.Buy(inventory, shop, goods, -1), "negative units");
ModAssert.False(TradeLedger.Buy(inventory, shop, "no/such_item", 1), "not on sale");
ModAssert.Equal(5, inventory.CheckCount(shop.Currency), "money untouched");
ModAssert.Equal(before, inventory.CheckCount(goods), "goods untouched");
}
[ModTest("ledger/sell_pays_and_removes_goods")]
static void SellPaysAndRemovesGoods(ModTestContext ctx)
{
var goods = Goods();
var shop = Shop(goods);
var inventory = new Inventory();
inventory.Add(goods, 4);
var before = inventory.CheckCount(goods);
var money = inventory.CheckCount(shop.Currency);
ModAssert.True(TradeLedger.Sell(inventory, shop, goods, 3), "owned units sell");
ModAssert.Equal(money + 6, inventory.CheckCount(shop.Currency), "3 x 2 coins paid");
ModAssert.Equal(before - 3, inventory.CheckCount(goods), "three units gone");
}
[ModTest("ledger/sell_refused_changes_nothing")]
static void SellRefusedChangesNothing(ModTestContext ctx)
{
var goods = Goods();
var shop = Shop(goods);
var inventory = new Inventory();
inventory.Add(goods, 2);
inventory.Add(shop.Currency, 7);
var before = inventory.CheckCount(goods);
shop.Buys.Add(new ShopOffer { Item = shop.Currency, Price = 1 });
ModAssert.False(TradeLedger.Sell(inventory, shop, goods, before + 1), "more than owned");
ModAssert.False(TradeLedger.Sell(inventory, shop, goods, 0), "zero units");
ModAssert.False(TradeLedger.Sell(inventory, shop, shop.Currency, 1), "money is never sold");
ModAssert.False(TradeLedger.Sell(inventory, new ShopInfo { ID = "picky" }, goods, 1), "not bought by this shop");
ModAssert.Equal(before, inventory.CheckCount(goods), "goods untouched");
ModAssert.Equal(7, inventory.CheckCount(shop.Currency), "money untouched");
}
[ModTest("ledger/max_counts")]
static void MaxCounts(ModTestContext ctx)
{
var goods = Goods();
var shop = Shop(goods);
var inventory = new Inventory();
inventory.Add(shop.Currency, 11);
inventory.Add(goods, 2);
ModAssert.Equal(3, TradeLedger.MaxBuy(inventory, shop, goods), "11 coins buy three at 3");
ModAssert.Equal(inventory.CheckCount(goods), TradeLedger.MaxSell(inventory, shop, goods), "the whole stack sells");
ModAssert.Equal(0, TradeLedger.MaxBuy(inventory, shop, "no/such_item"), "not on sale");
ModAssert.Equal(0, TradeLedger.MaxSell(inventory, new ShopInfo { ID = "picky" }, goods), "not bought");
ModAssert.Equal(0, TradeLedger.Money(null, shop), "no inventory, no money");
}
}
}