Yet Another Roguelike Tutorial, Part 8

Publish date: Aug 10, 2023
Tags: godot4 tutorial roguelike

Part 8: Items and Inventory

Welcome back to the roguelike tutorial series. This tutorial will continue from where the last one left off. You can find the previous tutorial here: https://selinadev.github.io/11-rogueliketutorial-07/

This part will tackle items, as well as inventory. As you might expect this will involve some (basic) inventory UI. As such we will get a lot of use out of Godot’s UI system. We will depart from the way the original python tutorial does things, as we did in the last part, and I will show you some more Godot tricks you may or may not be familiar with. I do have to admit, however, that what I present is at some points not the cleanest solution, but in fact quite hacky. Still, it gets you a working game, and maybe you can take it as an example of how to take a shortcut. Anyway, let’s get into the tutorial.

The first thing we need to do is define some more colors our message log. So in colors.gd add the following:

const INVALID = Color("ffff00")
const IMPOSSIBLE = Color("808080")
const ERROR = Color("ff4040")

const WELCOME_TEXT = Color("20a0ff")
const HEALTH_RECOVERED = Color("00ff00")

This shows us two things. First, we have some colors for impossible actions, meaning we will create a system to check if an action is possible at all, and if not we tell the player, and do not consume their turn. We will start with that right away and integrate that with all the actions we have had so far. You also see a color for the recovery of health. The item we will create and handle throughout this part will be a health potion, allowing the player to recover hp they have lost in a fight.

First, we prepare the fighter to heal. We will later need information how much hp were recovered, so we will expand fighter_component.gd by the following two functions:

func heal(amount: int) -> int:
	if hp == max_hp:
		return 0
	
	var new_hp_value: int = hp + amount
	
	if new_hp_value > max_hp:
		new_hp_value = max_hp
		
	var amount_recovered: int = new_hp_value - hp
	hp = new_hp_value
	return amount_recovered


func take_damage(amount: int) -> void:
	hp -= amount

The take_damage() function is pretty self explanatory, it just makes how the fighter component works more explicit. The heal() function first calculates how many hp are recovered, then sets the hp to the new values, after which it returns the calculated value.

Now we tackle the system of reporting impossible actions and preventing them from taking a turn. If you would start the game now and find an enemy, then run against the a wall, the enemy would receive a turn each time. We want to change our system so impossible actions are ignored by the turn order. Running against a wall is a free action, so to speak. This will be another point where we diverge from the original tutorial. That tutorial uses exceptions to handle such actions, which Godot does not have. So we need to improvise. The solution actually isn’t even that complicated, it just needs a (slightly tedious) refactor. First, in game.gd we change the _physics_process() function:

func _physics_process(_delta: float) -> void:
	var action: Action = await input_handler.get_action(player)
	if action:
		var previous_player_position: Vector2i = player.grid_position
		if action.perform():
			_handle_enemy_turns()
			map.update_fov(player.grid_position)

What we will do in a moment is give the actions' perform() function a boolean return value. We check that, and then only update the fov or handle enemy turns if it’s true. That means that we need to change action.gd:

func perform() -> bool:
	return false

As mentioned, the perform function now reports a bool value back, which will tell us if the action should consume a turn. If it didn’t happen (or if we decide to include some actions that don’t consume a turn by default), we return false, but if it should cost a turn we return true. Unfortunately this means we need to update all the actions we already have, which are a few at this point. Let’s start with melee_action.gd:

func perform() -> bool:
	var target: Entity = get_target_actor()
	if not target:
		if entity == get_map_data().player:
			MessageLog.send_message("Nothing to attack.", GameColors.IMPOSSIBLE)
		return false
	
	var damage: int = entity.fighter_component.power - target.fighter_component.defense
	var attack_color: Color
	if entity == get_map_data().player:
		attack_color = GameColors.PLAYER_ATTACK
	else:
		attack_color = GameColors.ENEMY_ATTACK
	var attack_description: String = "%s attacks %s" % [entity.get_entity_name(), target.get_entity_name()]
	if damage > 0:
		attack_description += " for %d hit points." % damage
		MessageLog.send_message(attack_description, attack_color)
		target.fighter_component.hp -= damage
	else:
		attack_description += " but does no damage."
		MessageLog.send_message(attack_description, attack_color)
	return true

Here we added some code right at the top. If we don’t have a target, we log a message that says so. However, our enemies also use the MeleeAction, and we don’t want to log messages for them, which is why we need to check if the player is the entity performing the action. If so, we can log it. Afterwards we return false. The only other change is that we appended return true to the end of the function. Moving on to movement_action.gd:

func perform() -> bool:
	var destination: Vector2i = get_destination()
	
	var map_data: MapData = get_map_data()
	var destination_tile: Tile = map_data.get_tile(destination)
	if not destination_tile or not destination_tile.is_walkable() or get_blocking_entity_at_destination():
		if entity == get_map_data().player:
			MessageLog.send_message("That way is blocked.", GameColors.IMPOSSIBLE)
		return false
	entity.move(offset)
	return true

We had the checks for exit early before, and here I combined all three into a single check. Again, the enemies move as well, so we need to check if the player is doing the action. If so, and if the action cannot be performed for any of the reasons we check for, we log a message telling the player that the way is blocked. Again, we return false in the abort path and return true at the end of function. Now for the wait_action.gd:

func perform() -> bool:
	return true

This one is easy. We just return true. The entire purpose of this action is to explicitly consume a turn without doing anything. We also need to handle the bump_action.gd:

func perform() -> bool:
	if get_target_actor():
		return MeleeAction.new(entity, offset.x, offset.y).perform()
	else:
		return MovementAction.new(entity, offset.x, offset.y).perform()

We simply have to return the return value of the sub-actions we perform. Lastly, the escape_action.gd:

func perform() -> bool:
	entity.get_tree().quit()
	return false

This is an odd one. The return statement is of no consequence here, because the game will stop running before there is another turn. Still, the function needs the return statement. With that change in place you should now be able to try that same thing from before, running into a wall in front of an enemy, and now you should see a message but not much else happening. And just like that we handled impossible actions, all without the need for exceptions. And with that we can get into starting with items. For now we will start with implementing consumables. We will tackle equipment in the last part of this series.

I mentioned before how I don’t make a clear distinction between different kinds of entities. For now, we’ll be able to pick up and consume everything that has a consumable component attached to it. Want to introduce an enemy made of cheese that can be eaten after it has been slain? Sure, just give it both the AI component and fighter component necessary for an enemy, as well as the healing consumable we’ll shortly make.

First, we need a base class for consumables. Create a new script extending Component at res://src/Entities/Actors/Components/consumable_component.gd. This is just a basic base class for extending later:

class_name ConsumableComponent
extends Component


func get_action(consumer: Entity) -> Action:
	return ItemAction.new(consumer, entity)


func activate(action: ItemAction) -> bool:
	return false

The get_action() method will certainly give you an error. We create the ItemAction in a moment. I’ll explain that more when we get to it. The activate() method will be used to do whatever the item can do. There’s a chance the item can’t (or shouldn’t) be used, like using a health potion when you’re at full hp. That’s why we have a boolean return value, which will integrate in the system that checks if an action has been performed. Now, how does the ItemAction look like? Create a new script extending Action at res://src/Entities/Actors/Actions/item_action.gd. Here’s the top of that script:

class_name ItemAction
extends Action

var item: Entity
var target_position: Vector2i

As you can see we store more than just the entity here. We also need to know the item we’re interacting with. Some items will also require a target position, which we’ll look at more in the next part.

func _init(entity: Entity, item: Entity, target_position = null) -> void:
	super._init(entity)
	self.item = item
	if not target_position is Vector2i:
		target_position = entity.grid_position
	self.target_position = target_position

Here you see that we give the additional parameters. Unfortunately, Vector2i is not nullable, so in order to be able to use null as a default argument we have to omit the type of target_position here. We set up the entity by invoking super._init(entity), then setting up the item ourselves here. If we didn’t pass in any target_position, we set it to the entities current position.

func get_target_actor() -> Entity:
	return get_map_data().get_actor_at_location(target_position)


func perform() -> bool:
	if item == null:
		return false
	return item.consumable_component.activate(self)

The get_target_actor() function is the same as we had in other classes before. perform() here checks if the item is not null, which should not happen in theory anyway. Then we call the activate() function on the item’s consumable component, and return whatever that returns. Here we also have a bit of a circular dependency (which makes deciding the order in which I write the tutorial pretty difficult). We call that on the consumable_component variable of item. We still have to create that. So in entity.gd add the following:

var fighter_component: FighterComponent
var ai_component: BaseAIComponent
var consumable_component: ConsumableComponent

We can now turn our attention to implementing a more useful variant of the consumable, one for healing. However, we will configure it with a configuration resource, as we did the other components, so we need two of those first. Create a new script extending Resource at res://src/Entities/Actors/Components/ComponentDefinitions/consumable_component_definition.gd. Here is the contents of that file:

class_name ConsumableComponentDefinition
extends Resource

Not very exciting, and you might wonder why we need that at all. Why not implement the healing consumable directly. The reason is polymorphism. In a bit we will add a field for a consumable definition to the entity definition. We want to be able to fill that field with any type of consumable definition. So we will define that field of this base type, which will allow us to fill it with all the various (actually useful) sub types. If you’re not getting it now, don’t worry. What I mean should become apparent once we actually define our healing potion. For now create another script, this time extenting ConsumableComponentDefinition at res://src/Entities/Actors/Components/ComponentDefinitions/healing_consumable_component_definition.gd. This will define a single variable storing the amount of healing:

class_name HealingConsumableComponentDefinition
extends ConsumableComponentDefinition

@export var healing_amount: int = 0

Now we’re ready to create the healing consumable. Create yet another new script, this time extending ConsumableComponent at res://src/Entities/Actors/Components/healing_consumable_component.gd. Here’s the top of the script:

class_name HealingConsumableComponent
extends ConsumableComponent

var amount: int


func _init(definition: HealingConsumableComponentDefinition) -> void:
	amount = definition.healing_amount

Pretty straight forward, we have an amount of healing we store, which we set from the definition we just created, which is passed into the _init() function. Now for the more interesting part:

func activate(action: ItemAction) -> bool:
	var consumer: Entity = action.entity
	var amount_recovered: int = consumer.fighter_component.heal(amount)
	if amount_recovered > 0:
		MessageLog.send_message(
			"You consume the %s, and recover %d HP!" % [entity.get_entity_name(), amount_recovered],
			GameColors.HEALTH_RECOVERED
		)
		return true
	MessageLog.send_message("Your health is already full.", GameColors.IMPOSSIBLE)
	return false

We take the consumer from the action that’s passed in. We then do the healing, and check how much we’ve healed for. If we actually did some healing, we create an appropriate log message and return true. Otherwise we create a log message noting that we’re at full health already, and return false, not consuming the item. You might notice that we did not actually consume the item in the other path either. That’s because that requires us to manipulate the inventory, which we haven’t created yet. But first, we need to make sure that consumable components are actually created on the entities. So first go into entity_definition.gd, to append our component definitions:

@export_category("Components")
@export var fighter_definition: FighterComponentDefinition
@export var ai_type: Entity.AIType
@export var consumable_definition: ConsumableComponentDefinition
@export var inventory_capacity: int = 0

We’ll only get to the inventory later, but it doesn’t hurt to include the inventory capacity here as well. Now, in set_entity_type() in entity.gd we add the following:

	if entity_definition.fighter_definition:
		fighter_component = FighterComponent.new(entity_definition.fighter_definition)
		add_child(fighter_component)
		
	if entity_definition.consumable_definition:
		if entity_definition.consumable_definition is HealingConsumableComponentDefinition:
			consumable_component = HealingConsumableComponent.new(entity_definition.consumable_definition)
			add_child(consumable_component)

After the chunk handling the fighter definition we add one for consumables. First we check if we have a consumable definition. Then we check if that definition is of type HealingConsumableComponentDefinition. If so, we use that to fill our consumable component variable with a new HealingConsumableComponent. Once we have more types of consumables we can expand this to check for the other types as well.

With all that in place we are actually ready to create our health potions. Create a new resource of type EntityDefinition at res://assets/definitions/entities/items/health_potion_definition.tres. Give it the NameHealth Potion”. Use the process we used before to select one of the flask icons as the Texture. As Color I used “#7f00ff”. Of course uncheck Is Blocking Movement, and set the Type to Item. Leave the Fighter Definition and AI Type alone, but click on the empty slot next to Consumable Definition. Here’s what I meant before. You now have the option to create a new ConsumableComponentDefinition, but you also have the option to create a new HealingConsumableComponentDefinition. That way we can fill this slot with the definition we need (we will get more in the next part), and have all these options presented to us in the editor. Do now create a new HealingConsumableComponentDefinition and set the Healing Amount to 4.

Now we can move to placing these items. So in dungeon_generator.gd expand the entity_types constant to include the definition of the health potion:

const entity_types = {
	"orc": preload("res://assets/definitions/entities/actors/entity_definition_orc.tres"),
	"troll": preload("res://assets/definitions/entities/actors/entity_definition_troll.tres"),
	"health_potion": preload("res://assets/definitions/entities/items/health_potion_definition.tres")
}

Then add the following line for a new exported variable:

@export_category("Entities RNG")
@export var max_monsters_per_room: int = 2
@export var max_items_per_room: int = 2

Then we expand _place_entities(). We need to do the same thing as we did with the monsters, just a different number of times and with placing different things. There is probably a clever way to do this, but I just copy pasted most of the function:

func _place_entities(dungeon: MapData, room: Rect2i) -> void:
	var number_of_monsters: int = _rng.randi_range(0, max_monsters_per_room)
	var number_of_items: int = _rng.randi_range(0, max_items_per_room)
	
	for _i in number_of_monsters:
		var x: int = _rng.randi_range(room.position.x + 1, room.end.x - 1)
		var y: int = _rng.randi_range(room.position.y + 1, room.end.y - 1)
		var new_entity_position := Vector2i(x, y)
		
		var can_place = true
		for entity in dungeon.entities:
			if entity.grid_position == new_entity_position:
				can_place = false
				break
		
		if can_place:
			var new_entity: Entity
			if _rng.randf() < 0.8:
				new_entity = Entity.new(dungeon, new_entity_position, entity_types.orc)
			else:
				new_entity = Entity.new(dungeon, new_entity_position, entity_types.troll)
			dungeon.entities.append(new_entity)
	
	for _i in number_of_items:
		var x: int = _rng.randi_range(room.position.x + 1, room.end.x - 1)
		var y: int = _rng.randi_range(room.position.y + 1, room.end.y - 1)
		var new_entity_position := Vector2i(x, y)
		
		var can_place = true
		for entity in dungeon.entities:
			if entity.grid_position == new_entity_position:
				can_place = false
				break
		
		if can_place:
			var new_entity: Entity = Entity.new(dungeon, new_entity_position, entity_types.health_potion)
			dungeon.entities.append(new_entity)

If you run the game now you should find up to two health potions per room. However, we can’t even pick them up. But how could we we don’t have anywhere to put them. So let’s start with the inventory. This is another component, so create a new script extending Component at res://src/Entities/Actors/Components/inventory_component.gd. Here’s the first half of the script:

class_name InventoryComponent
extends Component

var items: Array[Entity]
var capacity: int


func _init(capacity: int) -> void:
	items = []
	self.capacity = capacity

Pretty simple. We have an array that can hold entities, and a capacity telling us how much entities it can hold. We also create a drop() function right away:

func drop(item: Entity) -> void:
	items.erase(item)
	var map_data: MapData = get_map_data()
	map_data.entities.append(item)
	map_data.entity_placed.emit(item)
	item.map_data = map_data
	item.grid_position = entity.grid_position
	MessageLog.send_message("You dropped the %s." % item.get_entity_name(), Color.WHITE)

In this function, we first remove the item we want to remove from our items array. We obtain a reference to map_data. We add the item back into the entities array of our map_data. After the initial dungeon generation the map goes through all the entities and places them. If we drop an item now it won’t be attached to the Map note. Therefore we will have to create a signal on map_data that tells the Map node when entities are placed at a later time. Here we emit that signal. We also set the item’s map data reference to that of the entity that dropped it. This might seem irrelevant now, but later when we have multiple levels we will be able to take items from one level to the next, and drop them into a different map data than they originally are. We also set the position of the item to the position of the entity dropping it, so it’s dropped just below that entity. Lastly, we create a log message detailing what happened. Now, as mentioned we have to create a new signal in map_data.gd:

class_name MapData
extends RefCounted

signal entity_placed(entity)

Then in map.gd we modify the generate() function as follows:

func generate(player: Entity) -> void:
	map_data = dungeon_generator.generate_dungeon(player)
	map_data.entity_placed.connect(entities.add_child)
	_place_tiles()
	_place_entities()

Here, once we have the map_data we connect that entity_placed signal to the Entities node’s add_child() function. The signal also has the entity as an argument, which it will pass into add_child(). This means by simply emitting that signal, the entity will be added onto the map.

Let’s integrate the inventory into the entity. In entity.gd add a variable for a new component:

var fighter_component: FighterComponent
var ai_component: BaseAIComponent
var consumable_component: ConsumableComponent
var inventory_component: InventoryComponent

Again, we will handle this in set_entity_type():

	if entity_definition.consumable_definition:
		if entity_definition.consumable_definition is HealingConsumableComponentDefinition:
			consumable_component = HealingConsumableComponent.new(entity_definition.consumable_definition)
			add_child(consumable_component)
	
	if entity_definition.inventory_capacity > 0:
		inventory_component = InventoryComponent.new(entity_definition.inventory_capacity)
		add_child(inventory_component)

After the code we have created for the consumable above, we add a check to see if our inventory capacity is more than 0. If it isn’t it doesn’t make much sense to even give the entity an inventory, so we don’t. To make the player have an inventory, go into entity_definition_player.tres and set the Inventory Capacity to 26. We will have a system where we access the items in the inventory by assigning them letters, so with 26 letters in the English alphabet we have 26 spots in the inventory.

For picking up we want an array that’s filtered for things we can pick up, just like we had for actors. That’s why we add the following function to map_data.gd:

func get_items() -> Array[Entity]:
	var items: Array[Entity] = []
	for entity in entities:
		if entity.consumable_component != null:
			items.append(entity)
	return items

This is similar to how we did the actors array. We create a new array and fill it with all the entities from the entities array that do have a consumable component attached. With the inventory in place, we can start work on the item interactions. The first thing we need for that are some new input actions. We’ll just create all the ones we need right now. Go into Project > Project Settings and select the Input Map tab. Create tree new actions with the following names and associated keys:

Action Key
pickup G
drop D
activate I

Here it might be useful to not use the physical key code, i.e., the position on a US keyboard, but the equivalent key code, as these are mnemonics. So we remember that dropping is done with d, and while the key arrangement was more important for the directional keys, here it might be more important to remember which key it is. However, there are quite a few key arrangements out there, and because we are using the vim style direction keys (using the letter keys), we can’t rule out that there won’t be collisions in certain layouts. If we wouldn’t use those I would actually recommend using the key codes directly, but we don’t know if the d key is in the place of the h key in some language, which would result in a drop action every time a player using the vim direction keys would want to go left. So keep that option in mind for your own projects.

But now let’s turn our attention to an action for picking up all the potions we have laying around. Create a new script extending Action at res://src/Entities/Actors/Actions/pickup_action.gd. Here is the code:

class_name PickupAction
extends Action


func perform() -> bool:
	var inventory: InventoryComponent = entity.inventory_component
	var map_data: MapData = get_map_data()
	
	for item in map_data.get_items():
		if entity.grid_position == item.grid_position:
			if inventory.items.size() >= inventory.capacity:
				MessageLog.send_message("Your inventory is full.", GameColors.IMPOSSIBLE)
				return false
			
			map_data.entities.erase(item)
			item.get_parent().remove_child(item)
			inventory.items.append(item)
			MessageLog.send_message(
				"You picked up the %s!" % item.get_entity_name(),
				Color.WHITE
			)
			return true
	
	MessageLog.send_message("There is nothing here to pick up.", GameColors.IMPOSSIBLE)
	return false

In the action we first get the inventory of the entity performing the action. The player is the only entity with an inventory, and also the only entity that will be able to perform this action, so we expect this code to work. In a more complex game however we might want to check if there actually is an inventory component. (This is true for a lot of code here, but I wanted to note it here.) We then use our new map_data.get_items() function to loop through all the items. If we find one on the position of the performing entity, we next need to check if there is space in the inventory. If not, we note that in the message log and return false. Otherwise we remove the entity from the map_data, and also remove it as a child from the Map node. That way it will appear neither in our map data nor on the screen. We then just have to append it to the inventory’s items array, note the pickup in the message log and return true (because that is a successful action). Lastly, if we go through the whole loop without finding an item (and therefore returning early), we create a message that there is nothing to pick up, and then return false. Let’s now make use of that action. Go into main_game_input_handler.gd and insert a check for that in get_action() as follows:

	if Input.is_action_just_pressed("view_history"):
		get_parent().transition_to(InputHandler.InputHandlers.HISTORY_VIEWER)
	
	if Input.is_action_just_pressed("pickup"):
		action = PickupAction.new(player)	
	
	if Input.is_action_just_pressed("quit") or Input.is_action_just_pressed("ui_back"):
		action = EscapeAction.new(player)

If the pickup action is pressed we return a PickupAction, simple as that. You should now be able to run the game and move around the dungeon to find some health potions. Once you’re standing on a health potion you should be able to pick it up. You won’t be able to do anything with it, or even view the inventory. But that will require a new way to display and interact with items in the inventory. What we will do is to create a new UI element that we can populate with the items from our inventory, and that allows us to select one.

Create an new scene with a CanvasLayer as root node. Rename that node InventoryMenu, then save it at res://src/GUI/InventorMenu/inventory_menu.tscn. Add a CenterContainer as it’s child. Set that node to use the Full Rect. Add a PanelContainer as a child to it. Add a VBoxContainer as a child of the PanelContainer. To that we add three children: A Label node we will name TitleLabel, an HSeparator, and another VBoxContainer we call InventoryList. Set both the Title Label as well as the InventoryList to be accessible via unique names.

Now to the configuration. Go back into the game scene and select the StatsPanel. Scroll down to Theme Overrides > Styles > Panel, right click the StyleBoxTexture resource we have created there and click Copy. Then, back in our Inventory Menu scene, select the PanelContainer. Go to Theme Overrides > Styles > Panel on that node, click into the empty slot and select Paste. Now we have reused the panel border we already have.

For the TitleLabel create a new LabelSetting and fill it with “Kenney Pixel.ttf” again. Set both Horizontal and Vertical Alignment to Center. On the HSeparator go down to Theme Overrides > Styles > Separator, and create a new StyleBoxLine in that slot, and set its Color to white, to fit with the rest of our UI.

To create a list of items we will have to instantiate a ui element. We could do labels, but if we do buttons we can get a lot of the functionality we need for free. For these list items I often like to create a separate scene and configure them there, even if it’s just a single node, as I feel it’s easier to configure it in the editor than doing everything in code. So let’s do that. Create a new scene with a Button as root node. Rename that button to InventoryMenuButton and save it next to the other scene at res://src/GUI/InventorMenu/inventory_menu_item.tscn. Set Alignment to Left. Set Action Mode to Button Press. Then uncheck Shortcut Feedback. Under Container Sizing check the Horizontal Expand.

Now to the visuals. Scroll down to Theme Overrides. Change all the Font related colors to white. Set the Font to use “Kenney Pixel.ttf”. Under Style create a new StyleBoxEmpty in the Normal slot. Copy it to the Disabled Style by dragging and dropping. Then create a new StyleBoxFlat in the Hover slot. Uncheck Draw Center, set all the Border Width values to 1 px, and set the Border Color to white. Once all that’s configured copy that style box into the Pressed and Focussed slots. And that’s it, we can now proceed to code that interface.

I mentioned the inventory corresponding to letters before. What we will do is to associate each inventory item with a letter, which will be displayed next to the item name. We can then select an item we want to interact with by pressing that letter. We do that by defining that letter’s key as shortcut key for the button. However, as the items are represented by buttons, we can also use the mouse to select them, and it will work just the same. Even better, as the buttons are within a VBoxContainer godot automatically knows that the buttons are above one another, and as we have defined the focussed state of the buttons to be indicated with a border, we can even use the arrow keys or the tab key to go through that list, and Godot will automatically focus and thereby highlight the next item, and we can confirm our choice with the enter key. In the Inventory Menu scene attach a new script to InventoryMenu and save it at res://src/GUI/InventorMenu/inventory_menu.gd. Let’s start with that script:

class_name InventoryMenu
extends CanvasLayer

signal item_selected(item)

const inventory_menu_item_scene := preload("res://src/GUI/InventorMenu/inventory_menu_item.tscn")

@onready var inventory_list: VBoxContainer = $"%InventoryList"
@onready var title_label: Label = $"%TitleLabel"

As you can see we will emit a signal to report which item was selected. We also get a reference to the packed scene of the button we just configured, as well as references to the two nodes we will interact with, the title_label, as we will be able to configure that, as well as the inventory_list, which is just the VBoxContainer we drop all the buttons into.

func _ready() -> void:
	hide()

This scene will need to be configured before it’s used, so initially we hide it. The configuration should usually happen in the same frame as we instantiate this scene, so it’s probably not strictly necessary, but it doesn’t hurt to put this here either.

func button_pressed(item: Entity = null) -> void:
	item_selected.emit(item)
	queue_free()

This function will be called when a button is pressed. It will emit the associated item, then delete the item menu. We will spawn a new item menu every time we go into the inventory, so we need to get rid of it again.

func _register_item(index: int, item: Entity) -> void:
	var item_button: Button = inventory_menu_item_scene.instantiate()
	var char: String = String.chr("a".unicode_at(0) + index)
	item_button.text = "( %s ) %s" % [char, item.get_entity_name()]
	var shortcut_event := InputEventKey.new()
	shortcut_event.keycode = KEY_A + index
	item_button.shortcut = Shortcut.new()
	item_button.shortcut.events = [shortcut_event]
	item_button.pressed.connect(button_pressed.bind(item))
	inventory_list.add_child(item_button)

In order to fill our list with items, we create a helper function for individual items. _register_item() takes an integer index (the position in the list/inventory), as well as the item itself as arguments. First, we instantiate a button. Then we calculate a char variable. That’s the letter associated with the item. Unfortunately Godot doesn’t have a character type, so this looks a bit messy (the alternative would be to have a constant array containing the alphabet). What happens here is that we take the unicode representation of the letter “a”, then add the index to it. Within unicode the letter codes are sorted alphabetically, so by transforming that back into a string we get the appropriate letter (“a” + 0 is “a”, “a” + 1 is “b”, and so forth). Next we set the text of the button, by putting the char in parenthesis and appending the item name.

Then we handle setting up the shortcut. We create a new input event, whose keycode we set to KEY_A + index. The keycode enums are sorted alphabetically as well, so by adding the index to KEY_A we do in fact get the appropriate key code. We then create a shortcut resource and set the input event we created as the single event the shortcut will listen to.

Then we connect the button’s pressed signal to our button_pressed() function. That function expects an item argument, so we bind the item to that signal. That way all the information we need is encoded, no need to manually handle items later. Lastly, we add the button to the inventory_list. Now to building the complete list:

func build(title_text: String, inventory: InventoryComponent) -> void:
	if inventory.items.is_empty():
		button_pressed.call_deferred()
		MessageLog.send_message("No items in inventory.", GameColors.IMPOSSIBLE)
		return
	title_label.text = title_text
	for i in inventory.items.size():
		_register_item(i, inventory.items[i])
	inventory_list.get_child(0).grab_focus()
	show()

The build() function takes a text for the title, and of course the inventory it should display. Of course, the inventory could be empty. In that case we defer a call to button_pressed(). We need to use call_deferred() instead of calling it directly, because of how we call this function. Otherwise we would lock the game. I will come back to that when it becomes relevant. We also log a message telling the player why we won’t show an inventory. Then we simply return. The button_pressed() call will take care of freeing the menu.

If we do have stuff in the inventory, we set the title_label’s text, and then loop over all the indices of the items array, then call _register_item() for each of the items. We loop over the indices rather than the items directly, as we need to know them for registering the item. Then we have the first item grab the focus, which will make it highlighted. Lastly, we show the menu. So far we can select an item, but we need a way to not select an item and get back to the game.

func _physics_process(_delta: float) -> void:
	if Input.is_action_just_pressed("ui_back"):
		item_selected.emit(null)
		queue_free()

In _physics_process() we check if a “ui_back” action was pressed, and if so we emit item_selected with null and queue_free() the menu. We do have a menu that can let us select items, but before we get to implement it, we need to take care of a few other things first. We need to create the actions that will make use of the selected items, and after that we will put everything together. Create a new script extending ItemAction at res://src/Entities/Actors/Actions/drop_item_action.gd. Here’s its contents:

class_name DropItemAction
extends ItemAction


func perform() -> bool:
	if item == null:
		return false
	entity.inventory_component.drop(item)
	return true

In perform we actually have an item. We had our selection process return null if we didn’t or couldn’t select something, so we’re handling it here. If we do have an item, we call inventory_component.drop(), which we already implemented. That’s a proper action performed, so we return true.

Let us look at how to put this together before we move to using items. In the MainGameInputHandler, when the drop action is pressed, we will create our item selection menu and wait for it to report the selected item back. However, using this approach has a slight problem. We will use await, and while we await the item, the engine still happily calls _physics_process() on all other nodes, including our game which will try to fetch more actions from the MainGameInputHandler. That means without taking care of that the player could move their character while the item selection menu is open. There are several ways to circumvent this. We will exploit our state machine a bit, by creating a dummy input handler that does nothing, and which is active during this process.

So, create a new Node as a child of InputHandler and rename that node to DummyInputHandler. Take base_input_handler.gd and drag it onto the node, attaching that script. The base input handler does not really do anything, which is just what we want from our dummy. However, we need to account for it in our state machine. Modify the top of input_handler.gd as follows:

enum InputHandlers {MAIN_GAME, GAME_OVER, HISTORY_VIEWER, DUMMY}

@export var start_input_handler: InputHandlers

@onready var input_handler_nodes := {
	InputHandlers.MAIN_GAME: $MainGameInputHandler,
	InputHandlers.GAME_OVER: $GameOverInputHandler,
	InputHandlers.HISTORY_VIEWER: $HistoryViewerInputHandler,
	InputHandlers.DUMMY: $DummyInputHandler,
}

Now we can integrate that action in main_game_input_handler.gd. To spawn the inventory menu, we need a reference to the scene:


We need a helper function for that:

const directions = {
	"move_up": Vector2i.UP,
	"move_down": Vector2i.DOWN,
	"move_left": Vector2i.LEFT,
	"move_right": Vector2i.RIGHT,
	"move_up_left": Vector2i.UP + Vector2i.LEFT,
	"move_up_right": Vector2i.UP + Vector2i.RIGHT,
	"move_down_left": Vector2i.DOWN + Vector2i.LEFT,
	"move_down_right": Vector2i.DOWN + Vector2i.RIGHT,
}

const inventory_menu_scene = preload("res://src/GUI/InventorMenu/inventory_menu.tscn")

Next, we need a helper function that asynchronously can get us a selected item:

func get_item(window_title: String, inventory: InventoryComponent) -> Entity:
	var inventory_menu: InventoryMenu = inventory_menu_scene.instantiate()
	add_child(inventory_menu)
	inventory_menu.build(window_title, inventory)
	get_parent().transition_to(InputHandler.InputHandlers.DUMMY)
	var selected_item: Entity = await inventory_menu.item_selected
	await get_tree().physics_frame
	get_parent().call_deferred("transition_to", InputHandler.InputHandlers.MAIN_GAME)
	return selected_item

First, we instantiate our inventory menu, and add it as a child. Then we call build() to initialize it with the proper title and the inventory. After that we switch to the dummy input handler. Then we get the selected item with await. This will stop this function’s execution until inventory_menu emits the item_selected signal. Note how we only start listening for that signal two lines after building the menu. That is the reason why we used call_deferred() in the build() function. If we didn’t the inventory menu would emit that signal and only then would we stop everything until we get that signal. If we weren’t careful here an empty inventory could lock our game.

When the player selects an item or even when they exit the inventory menu they will press a button. So that button is in the just pressed state, meaning if we switch back to main game input handler right away, it could register those buttons as action inputs. To prevent that we wait for a frame using await get_tree().physics_frame, and then defer the transition back to the main game input handler. I can’t really explain why we need to wait and then call the transition deferred, but I had problems with these double inputs otherwise. Lastly, we return the selected item. With that helper function in place we can now expand get_action() with the code for our drop action:

    if Input.is_action_just_pressed("pickup"):
		action = PickupAction.new(player)
	
	if Input.is_action_just_pressed("drop"):
		var selected_item: Entity = await get_item("Select an item to drop", player.inventory_component)
		action = DropItemAction.new(player, selected_item)

You see we call get_item() with await. That stops us the function execution here until we get a return value. Once we have a return value, i.e., the selected item, we will continue by assigning a new DropItemAction with the selected item to our action. And with that, dropping should work. Run the project and try it out. You can now run around, collect health potions and then drop them again. That’s nice and all, but if you get hit by enemies while collecting the health potions, it would be nice if you could also use them.

But first, we need to modify our consumable a bit. Remember how our code wouldn’t get rid of a used health potion? We couldn’t do that when we implemented that, because there wasn’t an inventory yet from which to remove the consumed consumable. But now we can do that. Go to consumable_component.gd and add the following function:

func consume(consumer: Entity) -> void:
	var inventory: InventoryComponent = consumer.inventory_component
	inventory.items.erase(entity)
	entity.queue_free()

We simply get the consumer’s inventory, remove the item from the items array and then queue it free. Remember, entities are nodes, they are not RefCounted so we need to manage their lifetime ourselves. Now that we can consume consumables, let’s do so in healing_consumable_component.gd. Add a call to consume() as follows:

func activate(action: ItemAction) -> bool:
	var consumer: Entity = action.entity
	var amount_recovered: int = consumer.fighter_component.heal(amount)
	if amount_recovered > 0:
		MessageLog.send_message(
			"You consume the %s, and recover %d HP!" % [entity.get_entity_name(), amount_recovered],
			GameColors.HEALTH_RECOVERED
		)
		consume(consumer)
		return true
	MessageLog.send_message("Your health is already full.", GameColors.IMPOSSIBLE)
	return false

Back in main_game_input_handler.gd we can now add the following check to get_action():

	if Input.is_action_just_pressed("drop"):
		var selected_item: Entity = await get_item("Select an item to drop", player.inventory_component)
		action = DropItemAction.new(player, selected_item)
	
	if Input.is_action_just_pressed("activate"):
		var selected_item: Entity = await get_item("Select an item to use", player.inventory_component)
		action = ItemAction.new(player, selected_item)

Everything else was already in place, so we just need to select the item, like we did for the drop action, and then create an ItemAction. The basic item action already is set up to activate the consumable component of the item. If you run the game now, you should be able to go into your inventory with i. Once you have health potions they should show up there, and once you have sustained some damage you should be able to use them to heal some of that lost hp.

That concludes this part of the tutorial. Next time we will expand on our consumables with some magic scrolls. You can find the next part here: https://selinadev.github.io/13-rogueliketutorial-09/