Putting ground items in the inventory

When the player picks up an item, we need to check all inventory slots, see which are available and put it in an available one. If there are slots that already have the same item in them and there is room on the stack, we should prioritize them.

To do so, and to handle edge cases when picking up items, we need to write new functions. That’s what we’ll do in this lesson.

Finding a panel with a specific item

We have a blueprint on our ground item waiting for pick up, and it has a name as a string. We want to check if any inventory slots already have this name as its held item, so let’s code a lookup function in our GUI.

That’s another interaction that involves both the game world and the user interface, so we need to coordinate it through the GUI node.

The GUI has an InventoryWindow that contains several InventoryBar nodes. So we need to loop over the item slots in every bar and send relevant data up to the GUI that’ll process the information.

The place to start is the script that directly references each inventory panel: InventoryBar.gd. We write a function that returns all inventory slots matching an item’s name.

## Returns an array of inventory panels that have a held item that have a name that
## matches the item id provided.
func find_panels_with(item_id: String) -> Array:
    var output := []
    for panel in panels:
        # Check if there is an item and its name matches
        if panel.held_item and Library.get_entity_name_from(panel.held_item) == item_id:
            output.push_back(panel)

    return output

We then call that new function on each bar from InventoryWindow.gd.

## Returns an array of inventory panels that have a held item with a name matching
## the item id from the inventory bars.
func find_panels_with(item_id: String) -> Array:
    var output := []
    for inventory in inventories:
        output += inventory.find_panels_with(item_id)

    return output

While we don’t need the function in GUI.gd for picking up items, it will come in useful in the next chapter when we deal with crafting items and keeping track of the user’s inventory.

We can add a call this method and concatenate its return value with a call to quickbar.find_panels_with(). This gives us a list of all inventory slots with a specific item type, be it in the quick-bar or the inventory window.

## Returns an array of inventory panels containing a held item that has a name
## that matches the provided item id from the player inventory and quick-bar.
func find_panels_with(item_id: String) -> Array:
    var existing_stacks: Array = (
        quickbar.find_panels_with(item_id)
        + player_inventory.find_panels_with(item_id)
    )

    return existing_stacks

Adding an item into an existing panel

With that, we have the data we need to add items to existing stacks.

We’ll code that in GUI.gd.

First, we define a function to add an item to the inventory. It’s a bit long because we need to handle the following cases:

  1. If all inventory panels are full, we can’t add the item.
  2. If there’s a slot with an item of the same type and there’s space in the stack, we add it to the stack.
  3. If one stack isn’t enough to store the picked item, we find other stacks and empty slots to store it.
  4. If there’s no existing item of the same type, we find an empty slot and store it there.

Also, we prioritize inventory slots in this order:

  1. Existing stacks of this item.
  2. Empty slots in the quick-bar.
  3. Empty slots in the inventory window.

Let’s start with adding the item to the existing inventory stack. We’ll implement the other features as we move forward in the lesson.

To detect when the player picks up an item, we use our Event bus singleton and call add_to_inventory().

Open GUI.gd and add the following code to it.

func _ready() -> void:
    #...
    Events.connect("entered_pickup_area", self, "_on_Player_entered_pickup_area")


## Tries to add the blueprint to the inventory, starting with existing item
## stacks and then to an empty panel in the quickbar, then in the main inventory.
## Returns true if it succeeds.
func add_to_inventory(item: BlueprintEntity) -> bool:
    # This is temporary for testing! We'll replace this shortly.
    return false


## Tries to add the ground item detected by the player collision into the player's
## inventory and trigger the animation for it.
func _on_Player_entered_pickup_area(item: GroundItem, player: KinematicBody2D) -> void:
    if not (item and item.blueprint):
        return

    # We get the current amount inside the stack. It's possible for there to be
    # no space for the entire stack, but we could still pick up parts of the stack.
    var amount := item.blueprint.stack_count

    # Attempts to add the item to existing stacks and available space.
    if add_to_inventory(item.blueprint):
        # If we succeed, we play the `do_pickup()` animation, disable collision, etc.
        item.do_pickup(player)
    else:
        # If the attempt failed, we calculate if the stack is smaller than it
        # used to be before we tried picking it up.
        if item.blueprint.stack_count < amount:
            # If so, we need to create a new duplicate ground item whose job is to animate
            # itself flying to the player.
            var new_item := item.duplicate()

            # We need to use `call_deferred` to delay the new item by a frame because
            # we disable the shape's collision so it can't be picked up twice.
            #
            # As the physics engine is currently busy dealing with the collision
            # with the player's area and Godot doesn't allow us to change
            # collision states when its physics engine is busy, we need to wait
            # so it won't complain or cause errors.
            item.get_parent().call_deferred("add_child", new_item)
            new_item.call_deferred("setup", item.blueprint)
            new_item.call_deferred("do_pickup", player)

The call_deferred() method takes care of calling functions “during idle time”, that is to say, when the engine’s done with essential processes, like physics processing. It adds the calls to a queue and executes them as soon as the engine finished updating the world this frame.

Adding an item to the first empty inventory panel

If you run the game now, you can’t actually pick up items because we haven’t coded the logic to actually get them into the user’s inventory. We need to have the items either replenish existing stacks, or go into the first available empty panel.

We need a function to do those tasks. Open InventoryBar.gd where we can do that.

## Tries to add the provided item to the first available empty space. Returns
## true if it succeeds.
func add_to_first_available_inventory(item: BlueprintEntity) -> bool:
    var item_name := Library.get_entity_name_from(item)

    for panel in panels:
        # If the panel already has an item and its name matches that of the item
        # we are trying to put in it, _and_ there is space for it, we merge the
        # stacks.
        if (
            panel.held_item
            and Library.get_entity_name_from(panel.held_item) == item_name
            and panel.held_item.stack_count < panel.held_item.stack_size
        ):
            var available_space: int = panel.held_item.stack_size - panel.held_item.stack_count
            
            # If there is not enough space, we reduce the item count by however
            # many we can fit onto it, then move on to the next panel.
            if item.stack_count > available_space:
                var transfer_count := item.stack_count - available_space
                panel.held_item.stack_count += transfer_count
                item.stack_count -= transfer_count
            # If there is enough space, we increment the stack, destroy the item, and 
            # report success.
            else:
                panel.held_item.stack_count += item.stack_count
                item.queue_free()
                return true

        # If the item is empty, then automatically put the item in it and report success.
        elif not panel.held_item:
            panel.held_item = item
            return true

    # There is no more available space in this inventory bar or it cannot pick up
    # the item. Report as much.
    return false

Like we did with the find_panels_with() function, we need to forward the message from the GUI, starting with InventoryWindow.gd

## Adds the provided item to the first available spaces it can find in the
## inventory bars. Returns true if it succeeds.
func add_to_first_available_inventory(item: BlueprintEntity) -> bool:
    for inventory in inventories:
        if inventory.add_to_first_available_inventory(item):
            return true

    return false

And finally, we can replace GUI.gd’s temporary add_to_inventory() function with the real one by trying with the quick-bar first, then the inventory window. Remove the temporary return false and replace it with a call to the quick bar and inventory window.

func add_to_inventory(item: BlueprintEntity) -> bool:
    # If the item is already in the scene tree, remove it first.
    if item.get_parent() != null:
        item.get_parent().remove_child(item)

    if quickbar.add_to_first_available_inventory(item):
        return true

    return player_inventory.add_to_first_available_inventory(item)

Now, if you run the game and put down every battery, then deconstruct them and run over them, you’ll hoover them up right into your quick-bar as intended!

Code reference

Here are the complete scripts we modified in this lesson.

InventoryBar.gd

class_name InventoryBar
extends HBoxContainer

signal inventory_changed(panel, held_item)

export var InventoryPanelScene: PackedScene

export var slot_count := 10

var panels := []


func _ready() -> void:
    _make_panels()


func setup(gui: Control) -> void:
    for panel in panels:
        panel.setup(gui)
        panel.connect("held_item_changed", self, "_on_Panel_held_item_changed")


func find_panels_with(item_id: String) -> Array:
    var output := []
    for panel in panels:
        if panel.held_item and Library.get_entity_name_from(panel.held_item) == item_id:
            output.push_back(panel)

    return output


func add_to_first_available_inventory(item: BlueprintEntity) -> bool:
    var item_name := Library.get_entity_name_from(item)

    for panel in panels:
        if (
            panel.held_item
            and Library.get_entity_name_from(panel.held_item) == item_name
            and panel.held_item.stack_count < panel.held_item.stack_size
        ):
            var available_space: int = panel.held_item.stack_size - panel.held_item.stack_count
            
            if item.stack_count > available_space:
                var transfer_count := item.stack_count - available_space
                panel.held_item.stack_count += transfer_count
                item.stack_count -= transfer_count
            else:
                panel.held_item.stack_count += item.stack_count
                item.queue_free()
                return true

        elif not panel.held_item:
            panel.held_item = item
            return true

    return false


func _make_panels() -> void:
    for _i in slot_count:
        var panel := InventoryPanelScene.instance()
        add_child(panel)
        panels.append(panel)


func _on_Panel_held_item_changed(panel: Control, held_item: BlueprintEntity) -> void:
    emit_signal("inventory_changed", panel, held_item)

InventoryWindow.gd

extends MarginContainer

signal inventory_changed(panel, held_item)

var gui: Control

onready var inventory_path := $PanelContainer/MarginContainer/Inventories

onready var inventories := inventory_path.get_children()


func setup(_gui: Control) -> void:
    gui = _gui
    for bar in inventories:
        bar.setup(gui)

    var engine: BlueprintEntity = Library.blueprints.StirlingEngine.instance()
    engine.stack_count = 4
    var battery: BlueprintEntity = Library.blueprints.Battery.instance()
    battery.stack_count = 4
    inventories[0].panels[0].held_item = engine
    inventories[0].panels[1].held_item = battery


func claim_quickbar(quickbar: Control) -> void:
    quickbar.get_parent().remove_child(quickbar)
    inventory_path.add_child(quickbar)


func add_to_first_available_inventory(item: BlueprintEntity) -> bool:
    for inventory in inventories:
        if inventory.add_to_first_available_inventory(item):
            return true

    return false


func find_panels_with(item_id: String) -> Array:
    var output := []
    for inventory in inventories:
        output += inventory.find_panels_with(item_id)

    return output


func _on_InventoryBar_inventory_changed(panel, held_item) -> void:
    emit_signal("inventory_changed", panel, held_item)

GUI.gd

extends CenterContainer

const QUICKBAR_ACTIONS := [
    "quickbar_1",
    "quickbar_2",
    "quickbar_3",
    "quickbar_4",
    "quickbar_5",
    "quickbar_6",
    "quickbar_7",
    "quickbar_8",
    "quickbar_9",
    "quickbar_0"
]


var blueprint: BlueprintEntity setget _set_blueprint, _get_blueprint

var mouse_in_gui := false

onready var _is_open: bool = $HBoxContainer/InventoryWindow.visible

onready var player_inventory := $HBoxContainer/InventoryWindow
onready var _drag_preview := $DragPreview
onready var _gui_rect := $HBoxContainer

onready var quickbar := $MarginContainer/QuickBar
onready var quickbar_container := $MarginContainer



func _ready() -> void:
    player_inventory.setup(self)
    quickbar.setup(self)
    Events.connect("entered_pickup_area", self, "_on_Player_entered_pickup_area")



func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("toggle_inventory"):
        if _is_open:
            _close_inventories()
        else:
            _open_inventories()
    else:
        for i in QUICKBAR_ACTIONS.size():
            if InputMap.event_is_action(event, QUICKBAR_ACTIONS[i]) and event.is_pressed():
                _simulate_input(quickbar.panels[i])
                break


func find_panels_with(item_id: String) -> Array:
    var existing_stacks: Array = (
        quickbar.find_panels_with(item_id)
        + player_inventory.find_panels_with(item_id)
    )

    return existing_stacks


func add_to_inventory(item: BlueprintEntity) -> bool:
    if item.get_parent() != null:
        item.get_parent().remove_child(item)

    if quickbar.add_to_first_available_inventory(item):
        return true

    return player_inventory.add_to_first_available_inventory(item)


func _simulate_input(panel: InventoryPanel) -> void:
    var input := InputEventMouseButton.new()
    input.button_index = BUTTON_LEFT
    input.pressed = true

    panel._gui_input(input)


func _process(delta: float) -> void:
    var mouse_position := get_global_mouse_position()
    mouse_in_gui = _is_open and _gui_rect.get_rect().has_point(mouse_position)


func destroy_blueprint() -> void:
    _drag_preview.destroy_blueprint()


func update_label() -> void:
    _drag_preview.update_label()


func _set_blueprint(value: BlueprintEntity) -> void:
    if not is_inside_tree():
        yield(self, "ready")
    _drag_preview.blueprint = value


func _get_blueprint() -> BlueprintEntity:
    return _drag_preview.blueprint


func _open_inventories() -> void:
    _is_open = true
    player_inventory.visible = true
    player_inventory.claim_quickbar(quickbar)


func _close_inventories() -> void:
    _is_open = false
    player_inventory.visible = false
    _claim_quickbar()


func _claim_quickbar() -> void:
    quickbar.get_parent().remove_child(quickbar)
    quickbar_container.add_child(quickbar)


func _on_Player_entered_pickup_area(item: GroundItem, player: KinematicBody2D) -> void:
    if not (item and item.blueprint):
        return

    var amount := item.blueprint.stack_count

    if add_to_inventory(item.blueprint):
        item.do_pickup(player)
    else:
        if item.blueprint.stack_count < amount:
            var new_item := item.duplicate()

            item.get_parent().call_deferred("add_child", new_item)
            new_item.call_deferred("setup", item.blueprint)
            new_item.call_deferred("do_pickup", player)