Bug around reselling items if item quantity drops to 0

Maybe this gets fixed later, so for now ill keep going, but if i purchase everything from the stock it disappears completely. i cant even sell it back from the inventory. this seems to likely be related to the fact that the current system is really only designed to buy and sell items in the sellers dictionary of active items. Ill put my code at the bottom incase i screwed up… but i think this is a design thing and im hoping im just jumping the gun( if im wrong my error might have been in a much earlier lesson).

@Brian_Trotter later in the course do we fix the seller to be able to purchase items from us that are not in his inventory, or at least make it to where he holds a 0 value for items in his dictionary he is able to purchase?

   private Dictionary<InventoryItem, int> GetAvailabilities()
        {
            Dictionary<InventoryItem, int> availabilities = new Dictionary<InventoryItem, int>();

            foreach (var config in GetAvailableConfigs())
            {
                if(_isBuyingMode)
                {
                    if (!availabilities.ContainsKey(config._item))
                    {
                        int sold = 0;
                        _stockSold.TryGetValue(config._item, out sold);
                        availabilities[config._item] = -sold;
                    }
                availabilities[config._item] += config._initialStock;
                }
                else
                {
                    availabilities[config._item] = CountItemsInInventory(config._item);
                }
            }

            return availabilities;
        }

        private Dictionary<InventoryItem, int> GetPrices()
        {
            Dictionary<InventoryItem, int> prices = new Dictionary<InventoryItem, int>();

            foreach (var config in GetAvailableConfigs())
            {
                if(_isBuyingMode)
                {
                    if(!prices.ContainsKey(config._item))
                    {
                        prices[config._item] = config._item.GetPrice();
                    }
                    prices[config._item] = Mathf.CeilToInt(prices[config._item] * (1f - config._buyingDiscountPercentage / 100f));
                }
                else
                {
                    
                    
                        prices[config._item] = prices[config._item] = Mathf.CeilToInt(prices[config._item] * (1f - _sellPercentage / 100f));
                    
                }

            }
            return prices;

            
        }

        private IEnumerable<StockItemConfig> GetAvailableConfigs()
        {
            int shopperLevel = GetShopperLevel();
            foreach (var config in _stockConfig)
            {
                if (config.LevelToUnlock > shopperLevel) continue;
                yield return config;
            }
        }
        

        private void SellItem(Inventory shopperInventory, Purse shopperPurse, InventoryItem item, int price)
        {
            int slot = FindFirstItemSlot(shopperInventory, item);
            if (slot == -1) 
                {return;}
            AddToTransaction(item, -1);
            shopperInventory.RemoveFromSlot(slot, 1);
              if(_stockSold.ContainsKey(item) == false)
                {
                    //really really feels super hackey. could sell an item and havea negative stock sold. it works but yuck
                    _stockSold[item] = 0;
                }
            _stockSold[item]--;
            shopperPurse.UpdateBalance(price);
        }

    

        private void BuyItem(Inventory shopperInventory, Purse shopperPurse, InventoryItem item, int price)
        {
            if (shopperPurse.GetBalance() < price)
                {return;}


                bool success = shopperInventory.AddToFirstEmptySlot(item, 1);
            if (success)
            {
                AddToTransaction(item, -1);
                if(_stockSold.ContainsKey(item) == false)
                {
                    _stockSold[item] = 0;
                }
                _stockSold[item]++;
                shopperPurse.UpdateBalance(-price);
            }
        }

The items should disappear from inventory when all stock is purchased, but you should be able to sell it back.

We don’t address selling items not in the seller’s stock at all, actually.

This requires a slight reworking of the system, and there are three ways it can be handled:

The easy way: You can sell anything in inventory, but you can’t buy it back, the seller has clearly placed it in a processing bin to sell back to your enemies some other time. (i.e. forgotten, and not saved)

The not quite as easy way: You can sell anything in your inventory, and you can buy it back, but as soon as you quit the game or leave the scene, anything not in stock is sold to your enemies to use against you (i.e. not saved).

The hard way: You can sell anything in your inventory, and can buy it back, and if you come back tomorrow, it will still be in his inventory for you to buy back (this requires saving the non-stock items in addition to saving the normal stock transactions). This method doesn’t sell your loot back to your enemies to use against you at a later date.

I’ll post up examples this weekend (as I’ve actually done all three at one time or another).

Awesome thanks Brian. I’ll keep going through the course for now And circle back when you have posted. No reason for me to re-invent the wheel if you already have solutions you can pull. For the purposes of my game I don’t NEED sold item persistence, but I have been kindof over engineering at every point(both to learn, and to kindof have my own system in place I can use for the needs of multiple games).

LOL, I went through my code in SpellbornHunter, and discovered to my chagrin that I’ve entangled the shop code with a number of other classes unique to SH…

I’m going to re-create the solution without the entanglements.

Selling Items Not In Inventory, Item discarded after sell

This is by far the easiest thing to accomplish. It works by detecting if we're selling, and polling the Inventory for a collection of items to sell.

First, we need a struct to pass the inventory items off to the Shop.

namespace GameDevTV.Inventories
{
    [System.Serializable]
    public struct InventoryRecord
    {
        public InventoryItem item;
        public int amount;

        public InventoryRecord(InventoryItem item, int amount)
        {
            this.item = item;
            this.amount = amount;
        }
    }
}

This allows us, in Inventory, to expose a method that returns all of the Inventory in the form of an IEnumerable of InventoryRecords

        public IEnumerable<InventoryRecord> GetInventoryRecords()
        {
            foreach (InventorySlot slot in slots)
            {
                if (slot.item != null)
                {
                    yield return new InventoryRecord(slot.item, slot.number);
                }
            }
        }

Now technically, we could have made InventorySlot public and simply passed the slot (which, as a struct, would have been copied), but I chose to use a new InventoryRecord, because this will allow us to utilize this record in a version where we can buy back the items we just sold (because we like losing money! – or in case we were dumb and sold it).

The rest of the changes will all be in Shop.cs…

First, we may wish to have a different selling percentage for items that are in stock than for items that are not in stock… This reflects the notion that since the shop owner regularly sells these items, he can recoup the purchase price by selling it back to you rather than possibly losing money trying to sell it to a broker who will probably not want to buy it for full price before reselling it…

For example: If I sell Axes for a living, and you bring me an Axe, I already have an established reputation for selling Axes, so I don’t need to worry about marketing, or any other distractions, I can just put it in inventory and sell it to the next enemy who comes along to buy it. So I would be willing to pay a higher price for the item when the Player comes along to sell it because I’ll still make a profit. But if you bring me a Sword, well that’s not something I sell. Most customers know, they really want to go to Stark’s Stick Em With The Pointy End Sword Emporium to make this purchase instead. So I, as the Axe seller, will want to take this item to Stark’s as well to get it off of my hands… but Stark isn’t going to give me full price for it, and I still want to make some money, I’ve got 7 hungry kids and a pet HellHound to feed, after all. So I’m not going to buy that sword for the same percent of it’s retail value as I am the Axe.

Long story short, let’s add a [SerializedField] for nonStockSellingPercentage

        [SerializeField] private float nonStockSellingPercentage = 10f;

Now that that’s done, we need to make an adjustment to GetAvailableConfigs… When buying, we just want to use the stock configs, but when selling, we want to use the player’s Inventory…

        private IEnumerable<StockItemConfig> GetAvailableConfigs()
        {
            int shopperLevel = GetShopperLevel();
            if (isBuyingMode)
            {
                foreach (var config in stockConfig)
                {
                    if (config.levelToUnlock > shopperLevel) continue;
                    yield return config;
                }
            }
            else
            {
                foreach (InventoryRecord record in currentShopper.GetComponent<Inventory>().GetInventoryRecords())
                {
                    StockItemConfig newConfig = new StockItemConfig();
                    newConfig.item = record.item;
                    newConfig.initialStock = record.amount;
                    newConfig.buyingDiscountPercentage = nonStockSellingPercentage;
                    newConfig.levelToUnlock = 0;
                    foreach (StockItemConfig config in stockConfig)
                    {
                        if (config.item == newConfig.item && config.levelToUnlock<=shopperLevel)
                        {
                            newConfig.buyingDiscountPercentage = sellingPercentage + config.buyingDiscountPercentage/10f;
                        }
                    }
                    yield return newConfig;
                }
            }
                
        }

We’ve moved most of the logic from the original GetAvailableConfigs into the if(isBuyingMode) block…
In that case, we’re simply going through the items in the ShopConfigs and returning them if the player is high enough level to purchase them.

But if we’re not buying, we’re selling, so then we need to crawl through the Player’s inventory and create a new Config for each item within the inventory.

You’ll note in our newConfig, that we start by setting the buyingDiscountPercentage to the nonStockSellingPercentage. It doesn’t really matter that the field is named buyingDiscountPercentage, what’s important here is just that it gets set. We’re going to use this to calculate the correct price to buy the item back at later.

The levelToUnlock is set to 0 because the Player already has the item, so we’re not going to worry about if the player is high enough level or not. Just because I won’t sell you a beer till you’re 21 doesn’t mean I won’t buy it off you when you’re 16!

The next bit is simply to go through our stockConfigs and determine if the item exists there. If it does, then we we set the buyingDiscountPercentage to our sellingPercentage (the one for items we stock) + 1/10th of the normal buyingDiscountPercentage… Remember that we can have multiple entries with the same item in our stockConfig, and our system uses the last buyingDiscountPercentage it finds, so you can have items for 10% off at level 1 and 30%off at level 100 and as long as the level100 entry is after the level 1 entry, the buyingDiscountPercentage will be that last value. So if the buyingDiscountPercentage is 30% and the sellingPercentage is 50%, the new buyingDiscountPercentage (remember, this is really our sellingPercentage) is now 53%.

Finally, we yield return this config.

GetAvailableConfigs is used in two places: GetAvailabilities() and GetPrices().
For GetAvailablitities, no real change is needed, but we need a slight tweak in GetPrices() so that the selling price is properly calculated:

        private Dictionary<InventoryItem, float> GetPrices()
        {
            Dictionary<InventoryItem, float> prices = new Dictionary<InventoryItem, float>();

            foreach (var config in GetAvailableConfigs())
            {
                if (isBuyingMode)
                {
                    if (!prices.ContainsKey(config.item))
                    {
                        prices[config.item] = config.item.GetPrice() * GetBarterDiscount();
                    }

                    prices[config.item] *= (1 - config.buyingDiscountPercentage / 100);
                }
                else
                {
                    prices[config.item] = config.item.GetPrice() * (config.buyingDiscountPercentage / 100) * (1+GetBarterDiscount());
                }
            }

            return prices;
        }

So what we’re doing here is simply multiplying the item’s price by the config.buyingDiscountPercentage. Since we’ve already baked in the selling price based on whether or not it’s in the config, we only have to worry about the GetBarterDiscount() that is applied to the transaction. Note: I forgot about the GetBarterDiscount() in my initial draft, so the commit at the end of this post will not reflect the GetBarterDiscount() for selling, but it will be applied in a later commit when I deal with being able to buy items back!

And that’s about it. You can now sell any item in your inventory to any vendor, regardless of the vendor’s inventory.

GitLab Commit

1 Like

Thank you very much Brian. i will play with this Monday night and report back :). I’m eyeballs deep in a refactor of my turn system RN but wanted to thank you promptly as you are always so prompt with us!

1 Like

What if I told you that putting the item back into inventory is even easier than I was thinking it was?

When I wrote the system for SpellbornHunter, I wanted a system that I could buy the item back, but I didn’t care about saving because each level was procedurally generated. I did, however, have some other issues to deal with, like I also wanted procedurally generated stock inventory, and random stats on said inventory…

Without needing any of that other stuff saved, both making items available for resale, and more importantly, remembering those items by saving the stock are easy peasy. As a matter of fact, they’re joined at the hip since we’re saving the stockSold which is just a Dictionary<InventoryItem, int>.

Shops selling back, and saving non-stock items purchased

First up, we’re going to do a little refactoring… We’re going to need to grab the best stock configuration both in our buying loop and our selling loop, so I’ve extracted a method to get the best stock configuration of any particular item in the stockConfig:

        private bool GetBestConfig(InventoryItem item, int shopperLevel, out StockItemConfig bestConfig)
        {
            bool result = false;
            bestConfig = new StockItemConfig();
            foreach (StockItemConfig config in stockConfig)
            {
                if (config.item == item && config.levelToUnlock < shopperLevel)
                {
                    bestConfig=config;
                    result = true;
                }
            }
            return result;
        }

This method checks to see if there is a stockItemConfig that’s at least the shopper’s level, and returns the last one that it finds in the out parameter. If there is no stockConfig, then false is returned.

We’ll use that method in GetAvailableConfigs

        private IEnumerable<StockItemConfig> GetAvailableConfigs()
        {
            int shopperLevel = GetShopperLevel();
            if (isBuyingMode)
            {
                foreach (var config in stockConfig)
                {
                    if (config.levelToUnlock > shopperLevel) continue;
                    yield return config;
                }

                foreach (InventoryItem item in stockSold.Keys)
                {
                    if (stockSold[item] >= 0) continue;
                    if (!GetBestConfig(item, shopperLevel, out StockItemConfig bestConfig))
                    {
                        StockItemConfig newConfig = new StockItemConfig();
                        newConfig.item = item;
                        newConfig.levelToUnlock = 0;
                        newConfig.initialStock = 0;
                        newConfig.buyingDiscountPercentage = 0;
                        stockConfig.Add(newConfig);
                        yield return newConfig;
                    }
                }
            }
            else
            {
                foreach (InventoryRecord record in currentShopper.GetComponent<Inventory>().GetInventoryRecords())
                {
                    StockItemConfig newConfig = new StockItemConfig();
                    newConfig.item = record.item;
                    newConfig.initialStock = record.amount;
                    newConfig.buyingDiscountPercentage = nonStockSellingPercentage;
                    newConfig.levelToUnlock = 0;
                    if (GetBestConfig(newConfig.item, shopperLevel, out StockItemConfig bestConfig))
                    {
                        newConfig.buyingDiscountPercentage = sellingPercentage + bestConfig.buyingDiscountPercentage;
                    }
                    yield return newConfig;
                }
            }
        }

Note first in the else loops that rather than going through the StockConfigs directly, we’re testing to see if we get a “bestConfig”. If we do, then we use that bestConfig to set the buyingPercentage.

It’s the buying where we put this to the real test. After returning the available stockConfigs, we go through the stockSold (which is a Dictionary<InventoryItem, int>). If there is no entry in the stockConfig (which we can test with GetBestConfig(), if it returns false, then there is no StockItemConfig).

If there’s not, we create a StockItemConfig for the item with some default settings. We then add it to the stockConfig, as well as return it in the method. The next time the method is called, it will be in the stockConfig.

If you’ve pasted or typed these changes in, you might have noticed that you can’t .Add() to stockConfig. We need to make a small change to the definition of stockConfig.

        [SerializeField]
        List<StockItemConfig> stockConfig = new List<StockItemConfig>();

By changing this from a StockItemConfig[] to a List<StockItemConfig>, we can Add() the config to the List.

Now here’s the magic part. When we save the game, we’re already saving the Dictionary of items and quantity sold. When it’s restored, that same Dictionary is restored, even the items that we sold to the shop which are not in it’s stock config.

This post’s GitLab Commit

This topic was automatically closed 20 days after the last reply. New replies are no longer allowed.

Privacy & Terms