Adding Categories to Inventory System

Godot Version

4.2.1

Question

I’m trying to add tabs at the top of my inventory menu I’ve set up that can filter the viewable inventory by category. I’ve added category as a parameter of the items and can read the category of the item the cursor is on. The way I thought of for doing what I want to do is to duplicate the inventory arrays (plural because there’s an array of resources and an array of slots in the scene) for each category, then loop through each array and remove the elements that don’t match the category in question. Then I could update the shown inventory using of these new filtered arrays. Not sure if there’s a better way of doing that (if there is I’m all ears), but here’s the current code I have for my items_menu:

extends Control

@onready var category_cursor = $category_cursor
@onready var menu_cursor = $menu_cursor
@onready var pause_menu = get_parent()
@onready var pause_menu_holder = pause_menu.get_node("menu_holder")

@onready var inv : Inv = preload("res://inventory/player_inventory.tres")
@onready var inv_panel = get_node("menu_holder/VBoxContainer/inventory_panel")
@onready var inv_container = inv_panel.get_node("HBoxContainer/left_panel/MarginContainer/ScrollContainer/GridContainer")
@onready var slots : Array = inv_container.get_children()

@onready var item_slot = preload("res://inventory/inv_UI_slot.tscn")

@onready var focused_slot
@onready var focused_slot_name

@onready var canvas_layer = $CanvasLayer
@onready var use_item_confirmation = canvas_layer.get_node("use_item_confirmation")
@onready var confirmation_cursor = use_item_confirmation.get_node("menu_cursor")

@onready var sel_category : String

@onready var inv_beneficial : Array
@onready var inv_harmful : Array
@onready var inv_materials : Array
@onready var inv_tomes : Array
@onready var inv_weapons : Array
@onready var inv_armor : Array
@onready var inv_key : Array

@onready var beneficial : Array
@onready var harmful : Array
@onready var materials : Array
@onready var tomes : Array
@onready var weapons : Array
@onready var armor : Array
@onready var key : Array

func _ready():
	inv.update.connect(update_slots)
	inv.new_item_obtained.connect(add_new_slot)
	
	menu_cursor.focus_changed.connect(focus_on_new_slot)
	
	#get_viewport().gui_focus_changed.connect(focus_on_new_slot)
	focus_on_new_slot()
	
	visible = false
	category_cursor.cursor_active = false
	set_process(false)

func _process(_delta):
	update_slots()
	
	if inv.slots.size() > 1:
		inv.slots.sort_custom(sort_items_by_item_index)
	if slots.size() > 1:
		slots.sort_custom(sort_nodes_by_item_index)
	
	get_inv_categories()
	get_selected_category()
	
	update_slots()
	
	
	
	if Input.is_action_just_pressed("cancel") and !menu_cursor.cursor_waiting:
		category_cursor.cursor_active = false
		menu_cursor.cursor_active = false
		
		var pause_cursor = pause_menu.get_node("menu_cursor")
		pause_cursor.cursor_active = true
		pause_cursor.cursor_waiting = true
		pause_menu_holder.show()
		pause_menu_holder.set_process(true)
		
		hide()
		set_process(false)

func sort_items_by_item_index(a, b):
	if a.item.item_index < b.item.item_index:
		return true
	return false

func sort_nodes_by_item_index(a, b):
	if a.item_index < b.item_index:
		return true
	return false

func update_slots():
	if inv.slots.size() > slots.size():
		add_new_slot()
	
	slots = inv_container.get_children()
	
	if sel_category == "All":
		for i in range(min(inv.slots.size(), slots.size())):
			slots[i].update(inv.slots[i])
	if sel_category == "Beneficial":
		for i in range(min(inv.slots.size(), inv_beneficial.size())):
			slots[i].update(inv_beneficial[i])
		#print(inv_beneficial)

func add_new_slot():
	var instance = item_slot.instantiate()
	inv_container.add_child(instance)
	slots = inv_container.get_children()

# Runs when focus changes
func focus_on_new_slot():
	focused_slot = menu_cursor.menu_item
	if focused_slot:
		focused_slot_name = focused_slot.item_name
	
	for i in range(slots.size()):
		if slots[i].item_name == focused_slot_name:
			if !slots[i].cursor_selected.is_connected(item_selected):
				slots[i].cursor_selected.connect(item_selected)
			if !slots[i].purge_slot.is_connected(purge_slot):
				slots[i].purge_slot.connect(purge_slot)
		else:
			if slots[i].cursor_selected.is_connected(item_selected):
				slots[i].cursor_selected.disconnect(item_selected)
			if slots[i].purge_slot.is_connected(purge_slot):
				slots[i].purge_slot.disconnect(purge_slot)

func purge_slot(slot):
	var slot_index = inv.slots.find(slot, 0)
	inv.slots.remove_at(slot_index)

# Runs when an ite is selected
func item_selected():
	if !focused_slot.usable_from_field:
		return
	
	menu_cursor.cursor_active = false
	
	use_item_confirmation.show()
	use_item_confirmation.set_process(true)
	confirmation_cursor.cursor_active = true
	confirmation_cursor.cursor_waiting = true
	
	set_process(false)

func get_inv_categories():
	beneficial = slots.duplicate()
	for i in range(beneficial.size() - 1):
		if beneficial[i].item_category != "Beneficial":
			beneficial.remove_at(i)
	inv_beneficial = inv.slots.duplicate()
	for i in range(inv_beneficial.size() - 1):
		if inv_beneficial[i].item.category != "Beneficial":
			inv_beneficial.remove_at(i)

func get_selected_category():
	sel_category = category_cursor.menu_item.text

The most relevant parts of the code are the get_inventory_categories() and update_slots() functions, as that’s where I currently have my attempt at implementing this category system. With this code I can still see all my items with any category selected. Any help is appreciated!

Alrighty, after some more learning and experimenting I think I have it figured out. Got rid of one of the sets of arrays, only ended up needing one. For anyone who comes across a similar situation, here’s my code now that has been working so far:

extends Control

@onready var category_cursor = $category_cursor
@onready var menu_cursor = $menu_cursor
@onready var pause_menu = get_parent()
@onready var pause_menu_holder = pause_menu.get_node("menu_holder")

@onready var inv : Inv = preload("res://inventory/player_inventory.tres")
@onready var inv_panel = get_node("menu_holder/VBoxContainer/inventory_panel")
@onready var inv_container = inv_panel.get_node("HBoxContainer/left_panel/MarginContainer/ScrollContainer/GridContainer")
@onready var slots : Array = inv_container.get_children()

@onready var shown_inv : Array = inv.slots
@onready var current_cat : String

@onready var empty_item_slot = preload("res://inventory/empty_inv_UI_slot.tscn")
@onready var item_slot = preload("res://inventory/inv_UI_slot.tscn")

@onready var focused_slot
@onready var focused_slot_name

@onready var canvas_layer = $CanvasLayer
@onready var use_item_confirmation = canvas_layer.get_node("use_item_confirmation")
@onready var confirmation_cursor = use_item_confirmation.get_node("menu_cursor")

@onready var sel_category : String = current_cat
@onready var inv_empty : bool = true

@onready var inv_beneficial : Array
@onready var inv_harmful : Array
@onready var inv_materials : Array
@onready var inv_tomes : Array
@onready var inv_weapons : Array
@onready var inv_armor : Array
@onready var inv_key : Array

func _ready():
	inv.new_item_obtained.connect(add_new_slot)
	menu_cursor.focus_changed.connect(focus_on_new_slot)
	
	update_categories()
	focus_on_new_slot()
	
	visible = false
	category_cursor.cursor_active = false
	set_process(false)

func on_open():
	pass

func _process(_delta):
	slots = inv_container.get_children()
	
	# Function that adds or subtracts 'empty slots' as necessary
	adjust_empties()
	
	# Function that filters slots into a new array for each category
	get_inv_categories()
	# Function that updates sel_category to be what the cursor is on
	get_selected_category()
	# Update shown_inv to match the category's array
	update_categories()
	
	# Sort shown inventory if it contains more than 1 item
	if shown_inv.size() > 1:
		shown_inv.sort_custom(sort_items_by_item_index)
	
	# Correct slots size, then update contents to match shown_inv
	update_slots()
	
	if Input.is_action_just_pressed("cancel") and !menu_cursor.cursor_waiting:
		menu_cursor.set_cursor_from_index(0)
		category_cursor.set_cursor_from_index(0)
		
		category_cursor.cursor_active = false
		menu_cursor.cursor_active = false
		
		var pause_cursor = pause_menu.get_node("menu_cursor")
		pause_cursor.cursor_active = true
		pause_cursor.cursor_waiting = true
		pause_menu_holder.show()
		pause_menu_holder.set_process(true)
		
		hide()
		set_process(false)

func get_selected_category():
	sel_category = category_cursor.menu_item.text

func sort_items_by_item_index(a, b):
	if a.item.item_index < b.item.item_index:
		return true
	return false

func sort_nodes_by_item_index(a, b):
	if a.item_index < b.item_index:
		return true
	return false

func update_slots():
	if inv_empty:
		return
	if slots.size() < shown_inv.size():
		while slots.size() < shown_inv.size():
			var instance = item_slot.instantiate()
			inv_container.add_child(instance)
			slots = inv_container.get_children()
	if slots.size() > shown_inv.size():
		if inv_empty:
			while slots.size() > 1:
				inv_container.get_child(-1).free()
				slots = inv_container.get_children()
			slots[0].update(empty_item_slot)
		if !inv_empty:
			while slots.size() > shown_inv.size():
				inv_container.get_child(-1).free()
				slots = inv_container.get_children()
	
	for i in range(shown_inv.size()):
		slots[i].update(shown_inv[i])

func update_categories():
	if sel_category == "All":
		shown_inv = inv.slots
	if sel_category == "Beneficial":
		shown_inv = inv_beneficial
	if sel_category == "Harmful":
		shown_inv = inv_harmful
	if sel_category == "Materials":
		shown_inv = inv_materials
	if sel_category == "Tomes":
		shown_inv = inv_tomes
	if sel_category == "Weapons":
		shown_inv = inv_weapons
	if sel_category == "Armor":
		shown_inv = inv_armor
	if sel_category == "Key":
		shown_inv = inv_key
	
	current_cat = sel_category

func add_new_slot():
	var instance = item_slot.instantiate()
	inv_container.add_child(instance)
	slots = inv_container.get_children()
	focus_on_new_slot()

# Runs when focus changes
func focus_on_new_slot():
	menu_cursor.menu_item = menu_cursor.get_menu_item_at_index(menu_cursor.cursor_index)
	focused_slot = menu_cursor.menu_item
	
	if focused_slot:
		focused_slot_name = focused_slot.item_name
	
	for i in range(slots.size()):
		if slots[i].item_name == focused_slot_name:
			if !slots[i].cursor_selected.is_connected(item_selected):
				slots[i].cursor_selected.connect(item_selected)
			if !slots[i].purge_slot.is_connected(purge_slot):
				slots[i].purge_slot.connect(purge_slot)
		else:
			if slots[i].cursor_selected.is_connected(item_selected):
				slots[i].cursor_selected.disconnect(item_selected)
			if slots[i].purge_slot.is_connected(purge_slot):
				slots[i].purge_slot.disconnect(purge_slot)

func purge_slot():
	var slot_index = inv.slots.find(focused_slot.slot_to_purge, 0)
	inv.slots.remove_at(slot_index)

# Runs when an item is selected
func item_selected():
	focus_on_new_slot()
	if !focused_slot.usable_from_field:
		return
	
	menu_cursor.cursor_active = false
	
	use_item_confirmation.show()
	use_item_confirmation.set_process(true)
	confirmation_cursor.cursor_active = true
	confirmation_cursor.cursor_waiting = true
	
	set_process(false)

func get_inv_categories():
	inv_beneficial = inv.slots.filter(filter_for_beneficial)
	inv_harmful = inv.slots.filter(filter_for_harmful)
	inv_materials = inv.slots.filter(filter_for_materials)
	inv_tomes = inv.slots.filter(filter_for_tomes)
	inv_weapons = inv.slots.filter(filter_for_weapons)
	inv_armor = inv.slots.filter(filter_for_armor)
	inv_key = inv.slots.filter(filter_for_key)

func filter_for_beneficial(item):
	if item and item.item:
		return item.item.category == "Beneficial"
	return false

func filter_for_harmful(item):
	if item and item.item:
		return item.item.category == "Harmful"
	return false

func filter_for_materials(item):
	if item and item.item:
		return item.item.category == "Materials"
	return false

func filter_for_tomes(item):
	if item and item.item:
		return item.item.category == "Tomes"
	return false

func filter_for_weapons(item):
	if item and item.item:
		return item.item.category == "Weapons"
	return false

func filter_for_armor(item):
	if item and item.item:
		return item.item.category == "Armor"
	return false

func filter_for_key(item):
	if item and item.item:
		return item.item.category == "Key"
	return false

func adjust_empties():
	if slots.size() == 0:
		slots.append(empty_item_slot.instantiate())
		inv_empty = true
	if slots.size() == 1:
		if slots[0].is_empty == true:
			inv_empty = true
		else:
			inv_empty = false
	if slots.size() > 1:
		if slots.any(check_for_empties):
			var empty_node = inv_container.get_node("empty_inv_UI_slot")
			empty_node.free()
		inv_empty = false
	
	slots = inv_container.get_children()

func check_for_empties(slot):
	if slot:
		return slot.is_empty == true
	return false

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