Forum rules - please read before posting.

Possible to add/remove stackable items at the exact index you click

So there is something I noticed with AC and it seems to not handle stackable items more intuitively when adding or removing those items at the index you click on (Or at least I couldn't seem to find anything related to it).

For example, I have a stackable item "ItemA" which has a max stack of 5. I have a max stack of itemA in the first slot in the inventory and another full stack of itemA in the second slot in the inventory. I right click the second slot in the inventory and it uses and removes the item by 1 on the first slot instead. so the itemA in first slot now has 4 stacks when the second slot should have the stacks to 4 since I clicked on that one.

I know it sounds like a little thing but it seems like an odd behavior and I had to make my own custom Add item method to make it more intuitive. It tries to find the stackable item in the inventory and check if it has enough space and if it does then increment the count of that stack in the closest index otherwise add it normally.

Now the non stack type of items does have the ability to do things intuitively, for example the actionlist for Inventory > Add or Remove and using the Remove Method you can check the remove last selected box to get the desired behavior but if its a stackable item it will remove ALL of the stack and not give you the option to reduce the count by set amount.

So that's where I'm at, is there a built in method that handles stack items in that desired way?

Comments

  • Here is my custom Add Item method so it can handle stackable items. It seems to work so far but I still need to do more testing.

        public void AddItemToInventory(int itemId, int amount)
        {
    
            //create an instance of item that we need to add
            InvInstance itemToAdd = new InvInstance(itemId, amount);
    
            //Get the total inventory slots saved into a variable
            int maxInvenSlot = PlayerMenus.GetMenuWithName("InventoryNew").GetElementWithName("InventoryBox").GetNumSlots();
    
            //list to record all the index numbers where the item exists
            List<int> indexesThatContainStackItem = new List<int>();
    
            //variable used to identify which index is the closest to use for stackable items
            int closestIndex = -1;
    
            //If the inventory has no item, then simply add the item and return without needing to calculate other logic
            if (KickStarter.runtimeInventory.PlayerInvCollection.InvInstances.Count == 0)
            {
                Debug.Log("inventory is empty, no need to calculate further, just add the item");
                KickStarter.runtimeInventory.PlayerInvCollection.Add(itemToAdd);
                return;
            }
    
            //else If the inventory has 1 or more items
            else
            {
                if (KickStarter.runtimeInventory.PlayerInvCollection.InvInstances.Count <= maxInvenSlot)
                {
                    //If adding a stackable item
                    if (itemToAdd.InvItem.itemStackingMode == ItemStackingMode.Stack)
                    {
                        //If it doesn't contain the item in the inventory then add it it straight away without further logic
                        if (!KickStarter.runtimeInventory.PlayerInvCollection.InvInstances.Exists(item => item?.InvItem.id == itemToAdd.InvItem.id))
                        {
                            Debug.Log("Stackable item does not exist in inven, adding a new instance ");
                            KickStarter.runtimeInventory.PlayerInvCollection.Add(itemToAdd);
                            return;
                        }
    
    
                        //First Loop: this will assign the indexes to our indexesThatContainStackItem variable when it finds the item required to add in the inventory
                        for (int i = 0; i < KickStarter.runtimeInventory.PlayerInvCollection.InvInstances.Count; i++)
                        {
                            if (KickStarter.runtimeInventory.PlayerInvCollection.InvInstances[i]?.InvItem.id == itemId)
                            {
                                indexesThatContainStackItem.Add(i);
                            }
                        }
                        //Second Loop: This loop will figure out which one of the indexes is the closest and if its possible to stack on that same slot
                        for (int i = 0; i < indexesThatContainStackItem.Count; i++)
                        {
                            if (KickStarter.runtimeInventory.PlayerInvCollection.InvInstances[indexesThatContainStackItem[i]].InvItem.maxCount >= itemToAdd.Count + KickStarter.runtimeInventory.PlayerInvCollection.InvInstances[indexesThatContainStackItem[i]].Count)
                            {
                                closestIndex = indexesThatContainStackItem[i];
                                break;
                            }
                            else
                            {
                                //-1 indicates that NONE of the index numbers from the indexesThatContainStackItem list can be used because they are full stacks
                                closestIndex = -1;
                            }
                        }
                        //If no index were found that contained a stackable item that we can use then we add a new instance IF ONLY the inventory isn't full and the inventory does not contain any null instances
                        if (closestIndex < 0)
                        {
                            if (KickStarter.runtimeInventory.PlayerInvCollection.InvInstances.Count == maxInvenSlot && !KickStarter.runtimeInventory.PlayerInvCollection.InvInstances.Contains(null))
                            {
                                Debug.Log("Inventory full!. Cannot add item!");
                                return;
                            }
                            else
                            {
                                Debug.Log("Stackable Item was added");
                                KickStarter.runtimeInventory.PlayerInvCollection.Add(itemToAdd);
                                return;
                            }
                        }
                        //else apply the count increment to that inventory item using the closest index number
                        else
                        {
                            KickStarter.runtimeInventory.PlayerInvCollection.InvInstances[closestIndex].Count += itemToAdd.Count;
                            return;
                        }
    
    
                    }
                    //Adding other non stackable items. They do not need extra logic
                    else
                    {
                        if (KickStarter.runtimeInventory.PlayerInvCollection.InvInstances.Count == maxInvenSlot && !KickStarter.runtimeInventory.PlayerInvCollection.InvInstances.Contains(null))
                        {
                            Debug.Log("Inventory full!. Cannot add item!");
                            return;
                        }
    
                        //if its an item that you can only have 1 of
                        if (!itemToAdd.InvItem.canCarryMultiple)
                        {
                            if (!KickStarter.runtimeInventory.PlayerInvCollection.InvInstances.Exists(item => item.InvItem.id == itemToAdd.InvItem.id))
                            {
                                Debug.Log("Item was added");
                                KickStarter.runtimeInventory.PlayerInvCollection.Add(itemToAdd);
                                return;
                            }
                            else
                            {
                                Debug.Log("Could not add item, you can only have 1 of this item in your inventory");
                                return;
                            }
                        }
    
                        Debug.Log("Item was added");
                        KickStarter.runtimeInventory.PlayerInvCollection.Add(itemToAdd);
                        return;
                    }
                }
            }
    
        }
    
  • edited February 24

    I think the main issue here is that, if I understood you correctly, your right click is simply running an actionlist with the add/remove action, and the actionlist doesn't "know" which instance the click came from. My approach would probably be to code this into my custom inventory script - a right click would select the clicked item, run clickedInstance.Use(), and then remove the selected instance. The actionlist then would only take care of the actual effects of using the item (adding health if the item is bandages, for example).

    But it'd be cool if we had a "remove (currently selected)" action that removed only whatever was selected at the time. It wouldn't be useful in this specific situation, but I can see how it would be good to have.

  • edited February 24

    @Rairun Hello again, thanks for your help with the custom inventory script before.

    As for your advice, I have already tried that when I was trying to figure out why the checking for item category actionlist wasn't working with the custom inventory script.

    Here is a small snippet when right clicking to use item with my custom inventory script

                    // Use action by right clicking
                    if (buttonPressed == 2)
                    {
    
                        if (inventoryBox.title == "ContainerItems")
                        {
                            Debug.Log("Not allowed to use item in container");
                            return;
                        }
    
    
                        //NOTE: SelectItem and Deselect used so that the actionlist for Inventory > Check selected action is able to work
                        //when it comes to checking item category for that item. Since it requires to know the selected item data we need to select
                        //the clickedinstance and then deselect it after use because we don't want to be holding that item when we use it
                        KickStarter.runtimeInventory.SelectItem(clickedInstance);
                        clickedInstance.Use(0);
                        KickStarter.runtimeInventory.SelectedInstance?.Deselect();
    
                        return;
                    }
    
                }
    
  • edited February 24

    Just so I can clarify the title for this for my desired behavior.

    Adding stackable item to your inventory should first check if there is already existing item and if it has space then it should just increment that slot's stack count. That is what my custom Adding item method above does.

    And removing stackable items when you use them by right clicking (which is done by my custom inventory script), it should use the inventory slot's item that you clicked on if you have multiple same stackable items but in different slots.

  • edited February 24

    Regarding the original issue: how are you getting the behaviour of the wrong stack being updated when clicking? Is this from AC's built-in behaviour, a custom Action via the Item's "Standard interactions", or a hook into the OnMenuElementClick event?

    If it's AC's behaviour with no custom scripting, please share shots of your Managers so that I can recreate the behaviour.

    If it's via an event hook, OnMenuElementClick supplies the slot that was clicked on. This can be used to identify the corresponding ItemInstance with:

    myInventoryBox.GetInstance (clickedSlot);
    
  • @ChrisIceBox I am using a custom inventory script that handles the controls. So let me first show you my custom inventory.

    public class CustomInventory : MonoBehaviour
    {
        private void OnEnable()
        {
            EventManager.OnMenuTurnOn += OnMenuTurnOn;
            EventManager.OnMenuElementClick += OnMenuElementClick;
        }
        private void OnDisable()
        {
            EventManager.OnMenuTurnOn -= OnMenuTurnOn;
            EventManager.OnMenuElementClick -= OnMenuElementClick;
        }
    
        private void OnMenuTurnOn(AC.Menu menu, bool isInstant)
        {
            Debug.Log("OnMenuTurnOn Called");
            if (menu.title != "Container")
            {
                return;
            }
    
            MenuInventoryBox containerElement = menu.GetElementWithName("ContainerItems") as MenuInventoryBox;
            containerElement.OverrideContainer = KickStarter.playerInput.activeContainer;
            menu.Recalculate();
    
        }
    
        private void OnMenuElementClick(Menu menu, MenuElement element, int slot, int buttonPressed)
        {
            Debug.Log("OnMenuElementClick Called");
    
            MenuInventoryBox inventoryBox = element as MenuInventoryBox;
    
            InvInstance clickedInstance = inventoryBox?.GetInstance(slot);
    
            InvInstance selectedInstance = KickStarter.runtimeInventory.SelectedInstance;
    
    
            InvCollection collectionClicked;
            InvCollection collectionSelected;
    
    
            //Before proceeding, Check if the inventory box menu is not null and the inventoryBoxType setting is set to CustomScript
            if (inventoryBox != null && inventoryBox.inventoryBoxType == AC_InventoryBoxType.CustomScript)
            {
    
                // Click on an occupied slot while holding another item
                if (InvInstance.IsValid(clickedInstance) && InvInstance.IsValid(selectedInstance))
                {
    
                    collectionClicked = clickedInstance.GetSource();
    
                    collectionSelected = selectedInstance.GetSource();
    
                    // Swap items if the selected item came from a different collection (if multiple can be carried, add to stack)
                    if (collectionClicked != collectionSelected)
                    {
    
                        // Do nothing when items are the same and multiples are not allowed
                        if (!clickedInstance.InvItem.canCarryMultiple && clickedInstance.InvItem.maxCount < 2 && selectedInstance.ItemID == clickedInstance.ItemID)
                        {
                            KickStarter.runtimeInventory.SetNull();
                            return;
                        }
    
                        collectionClicked.Insert(selectedInstance, slot, OccupiedSlotBehaviour.SwapItems);
                        KickStarter.runtimeInventory.SetNull();
                        return;
                    }
                    // Combine items if they both come from the same collection
                    else
                    {
                        // Dealing with stackable items
                        if (clickedInstance.InvItem.canCarryMultiple && clickedInstance.InvItem.maxCount > 1 && selectedInstance.ItemID == clickedInstance.ItemID)
                        {
                            // Merge stacks
                            if (buttonPressed == 1)
                            {
                                Debug.Log("Item was merged with existing item in inventory");
                                collectionClicked.Insert(selectedInstance, slot, OccupiedSlotBehaviour.SwapItems);
                                KickStarter.runtimeInventory.SetNull();
                                return;
                            }
                            // Take one from stack
                            if (buttonPressed == 2)
                            {
                                KickStarter.runtimeInventory.SelectItem(clickedInstance);
                                return;
                            }
                        }
                        //Combine the selected item with the clicked item
                        selectedInstance.Combine(clickedInstance, false);
                        return;
                    }
                }
    
                // Place item in free slot
                if (!InvInstance.IsValid(clickedInstance) && InvInstance.IsValid(selectedInstance))
                {
    
                    InvCollection collection = (inventoryBox.title == "ContainerItems") ? inventoryBox.OverrideContainer.InvCollection : KickStarter.runtimeInventory.PlayerInvCollection;
    
                    collection.Insert(selectedInstance, slot, OccupiedSlotBehaviour.SwapItems);
                    KickStarter.runtimeInventory.SetNull();
                    return;
                }
    
                // Click on an item without holding anything
                if (InvInstance.IsValid(clickedInstance) && !InvInstance.IsValid(selectedInstance))
                {
                    // Dealing with stackable items
                    if (clickedInstance.InvItem.canCarryMultiple && clickedInstance.InvItem.maxCount >= 1)
                    {
                        // Take all by left clicking
                        if (buttonPressed == 1)
                        {
                            int count = clickedInstance.Count;
                            while (count > 0)
                            {
                                KickStarter.runtimeInventory.SelectItem(clickedInstance);
                                count--;
                            }
                            return;
                        }
    
                        // Take one from stack by right clicking
                        if (Input.GetKey(KeyCode.LeftShift) && buttonPressed == 2)
                        {
    
                            KickStarter.runtimeInventory.SelectItem(clickedInstance);
                            return;
                        }
                    }
    
                    // Use action by right clicking
                    if (buttonPressed == 2)
                    {
    
                        if (inventoryBox.title == "ContainerItems")
                        {
                            Debug.Log("Not allowed to use item in container");
                            return;
                        }
    
                        //NOTE: SelectItem and Deselect used so that the actionlist for Inventory > Check selected action is able to work
                        //when it comes to checking item category for that item. Since it requires to know the selected item data we need to select
                        //the clickedinstance and then deselect it after use because we don't want to be holding that item when we use it
                        KickStarter.runtimeInventory.SelectItem(clickedInstance);
                        clickedInstance.Use(0);
                        KickStarter.runtimeInventory.SelectedInstance?.Deselect();
    
                        return;
                    }
    
                }
    
            }
    
        }
    
    }
    

    At the end of the code, you can see that right clicking an item calls the use method
    clickedInstance.Use(0);
    And then this is the actionlist it calls and runs. These are not custom actionlist. It's built in ones.

    If I checked the "Remove last-selected" box, it removes the item from the correct index that I right click on BUT it removes ALL of the items and does not give you an option to set an amount to reduce by.

  • edited February 24

    @Shadowz, what I meant is that you could have something like this:

                    // Use action by right clicking
                    if (buttonPressed == 2)
                    {
    
                        if (inventoryBox.title == "ContainerItems")
                        {
                            Debug.Log("Not allowed to use item in container");
                            return;
                        }
    
    -
                        clickedInstance.Use(0);             
                        collectionClicked = clickedInstance.GetSource();
                        collectionClicked.Delete(clickedInstance, 1);
    
                        return;
                    }
    
                }
    

    Obviously you might not want the same behaviour for every type of item, but then I think you can easily use conditionals to limit the item deletion to the ones you want.

  • @Rairun I appreciate it, that works and seems to solve my problem but like you said different type of items will require conditional statements and there might be few other test cases/scenarios where it will either be too reliant on the custom inventory script (which could make it easier to break other aspects, cause weird behaviors or would require some more code for additional stuff).

    I would rather let "clickedInstance.Use(0);" handle things like removing (and other stuff) that can be easily done in the actionlist editor and it would be easier and more flexible to work with.

    I might use your way as a last resort if its doable and doesn't cause too much complications but I will only know this as I continue to work on my project and see if it suits my needs and desired behaviors further down the line. But thank you for that. I really appreciate it.

  • Yeah, I get you. Having a "reduce count by" box for "remove last selected?" would be useful for sure.

  • edited February 25

    @ChrisIceBox Which Manager do you require? the Inventory one?

    Also, I tried to recreate the problem using the default setup on AC on a new project and it seems like even if its Built in and not using custom inventory script, it still has the same problem.

    Here is a quick demonstration, Started a new game setup wizard. And then I made sure that I checked the Items can be reordered in menus.

    I created a new item to test out, put the Selection mode to Stack. Gave it a capacity of 5.

    I then added a button in scene for testing purposes. It calls this method:

        public void AddItem(int itemId)
        {
            InvInstance itemToAdd = new InvInstance(0,1);
            KickStarter.runtimeInventory.PlayerInvCollection.Add(itemToAdd);
        }
    

    Then I click on the button 4 times to add 4 to my inventory. (This is the default AC inventory, doesn't use custom script)

    If you then move the 4 stack items to the second slot in inventory

    And then try to add the item again to the inventory you will notice that rather than applying that item into the slot 2 item since it already exists and has enough space to carry 1 more, it instead makes a new one in the first slot.

    So this is just an example for the Adding stackable item problem. The similar issue happens with the removing of stack item. The difference is that since the control is to right click to use item in inventory, it requires a custom inventory script and then the clickedInstance.Use(0); handles the removing of item except if you have two slots of full stack of same item in the inventory, it will use the first slot it can find even if you click on a second slot that has the same item.

    Ex. Right clicking the item to use

    Result:

    Again, the clickedInstance.Use(0) calls the actionlist that uses AC's built in Remove method, but it doesn't allow you to set amount to remove if you have remove last selected checked.

  • edited February 25

    Thank you for the details.

    The example you give still uses custom scripting to add the item to the inventory, and that's where you need to be explicit in telling AC where exactly you want to add the item to.

    Instead of calling the InvCollection script's Add function, you can instead use Insert along with the index you want to add the new item to:

    private void AddItem (int itemId)
    {
        InvInstance itemToAdd = new InvInstance (itemId);
        InvCollection invCollection = KickStarter.runtimeInventory.PlayerInvCollection;
    
        InvInstance existingInstance = invCollection.GetFirstInstance (itemId);
        if (InvInstance.IsValid (existingInstance))
        {
            int existingIndex = invCollection.IndexOf (existingInstance);
            invCollection.Insert (itemToAdd, existingIndex);
        }
        else
        {
            invCollection.AddToEnd (itemToAdd);
        }
    }
    

    When it comes to removing items, you don't need to handle this through AC Actions - you can also do this via the Delete function as @Rairun notes.

    it doesn't allow you to set amount to remove if you have remove last selected checked.

    This is a good point, and I shall look into see if this can be modified.

  • edited February 25

    @ChrisIceBox If I add the item by count 1 using the built in AC actionlist, it still has the same issue.

    Thank you for that code, it does work if you have the count set as 1 but if you try anymore than that then it will break. I tried changing the count to 2 and when I tried to add the item while my stack was at 4/5, it added 1 to it making it 5/5 but the other one just disappeared and nowhere to be seen.

    Where as I am kind of expecting it to behave like if it can find any space in any other slots then it can merge with it, or if it has some space but not enough for the amount you are adding then it should add whatever you can fit into the stack until it becomes full and then add the leftovers in a new slot.

    I feel like the stack items need a couple layer of extra logic to work smartly, will we be able to see hopefully a better handling of it in future updates perhaps? It would also be nice to get access and some control for the LastClickedInstance in the built in actionlist.

    In the meantime, I guess I will try to make a custom method and then turn it into actionlist too.

  • If I add the item by count 1 using the built in AC actionlist, it still has the same issue.

    If you're not involving custom scripting, then the default click behaviour of InventoryBox elements will handle stacking. If you don't involve ActionLists, left-clicking a "Stack" item will take one, right-clicking will put one back.

    Where as I am kind of expecting it to behave like if it can find any space in any other slots then it can merge with it

    There's a lot of variation in what expected behaviour will be, so AC's inventory system is designed to be flexible to cater for a wide range - but it does mean that custom scripting needs to make that behaviour clear.

    The Insert function's third, optional, parameter dictates the behaviour when adding to a slot that is occupied. This function also returns the added item as a new InvInstance class - you can read this instance's Count property to determine how many were actually transferred. For example:

    int existingIndex = invCollection.IndexOf (existingInstance);
    int originalCount = itemToAdd.Count;
    InvInstance addedInstance = invCollection.Insert (itemToAdd, existingIndex);
    if (addedInstance.Count < originalCount)
    {
        InvInstance extraInstance = new InvInstance (itemId, originalCount - addedInstance.Count);
        invCollection.AddToEnd (extraInstance);
    }
    
Sign In or Register to comment.

Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

Welcome to the official forum for Adventure Creator.