7thSage again. Welcome back to part 10 of our tutorial. This time we’ll be working on setting up the ability to manage items and equipment. Last time we added stats, now lets make some more use of them. We’re now able to add things like increasing attack when we equip our sword, or recover health if we use a potion.
Feature
The main purpose of an item is to modify something, we’ll call this it’s feature. Every item will have at least one, or sometimes multiple features.
The base feature class will support using items in two main ways.
- We’ll be able to activate the effect of the item and later de-activate the effect. Such as equipping a piece of equipment, or a buff that lasts so many turns. We’ll use the Activate() and Deactivate() functions for these.
- The second use case is that we apply a permanent effect on the stat. Such as adding EXP to a unit after a battle, or HP after using a health potion. For this we’ll call the Apply() function.
We’ll implement what happens in OnApply() and OnRemove() in the classes that extend Feature.
Create a folder in “Scripts->View Model Component” named “Features” and inside it create a script named “Feature.gd”
extends Node class_name Feature var target:Node: get: return _target var _target:Node func Activate(targetObj:Node): if _target == null: _target = targetObj OnApply() func Deactivate(): if _target != null: OnRemove() _target = null func Apply(targetObj:Node): _target = targetObj OnApply() _target = null func OnApply(): pass func OnRemove(): pass
Stat Modifier Feature
We’ll likely use many different types of features to modify a large array of things besides just stats. We may have something that revives an ally, or adds a status ailment or many other possibilities. For now though, we’ll focus on creating a feature to modify stat values, and in future lessons we may tackle some of the other possibilities.
This one is fairly simple. Here we can modify any stat type, and depending on the value we send, could be either a good or bad effect. This is also compatible whether we are using a consumable item, or something that we will equip.
Create a new script named “StatModifierFeature.gd” in the same folder as the previous script.
extends Feature class_name StatModifierFeature var type:StatTypes.Stat var amount:int var stats:Stats: get: return _target.get_node("Stats") func OnApply(): var startValue = stats.GetStat(type) stats.SetStat(type, startValue + amount) func OnRemove(): var startValue = stats.GetStat(type) stats.SetStat(type, startValue - amount)
Merchandise
While we are talking about items. At some point we’ll also need to have a way to acquire them. Here we can add an atribute to an item to track its buy and sell price. We won’t be using it in this lesson, but it may be useful in the future.
Create a folder named “Item” in the folder “Script->View Model Component” and inside create a script named “Merchandise.gd”
extends Node class_name Merchandise var buy:int var sell:int
Consumable
We can give items the ability to be consumed by adding a Consumable component. We’ll call Apply() on any features attached to the object. Because the effects are not tied to an equipment status like equipping a piece of armor, we aren’t using Activate() or Deactivate(). We specify a target here because it shouldn’t be assumed that an item is being used on the user. We could be using the item on an ally or even an enemy, depending on the item. I’m using “.get_parent().get_children()” to get a list of all the siblings of a node. Our hierarchy for this lesson is set up like the following image.
![](https://i0.wp.com/theliquidfire.com/wp-content/uploads/2024/10/10Godot_01.png?resize=656%2C598&ssl=1)
Create a script named “Consumable.gd” in the folder “Scripts->View Model Component->Item”
extends Node class_name Consumable func Consume(targetObj:Node): var features:Array[Node] = [] features = self.get_parent().get_children() var filteredArray = features.filter(func(node):return node is Feature) for node in filteredArray: node.Apply(targetObj)
Equip Slots
Before we can work on equipping items, we need to have someplace to equip them to. We’ll create an enum that lists the various locations an item can be equipped, and like a previous lesson, we’ll use bit masks to identify the slot. This will let us do things like equip a sword two handed and know whether a shield is in the offhand that needs to be unequipped as well.
In the folder “Scripts->Enums Exentions” create a new script named “EquipSlots.gd”
class_name EquipSlots enum Slot { NONE = 0, PRIMARY = 1 << 0, SECONDARY = 1 << 1, HEAD = 1 << 2, BODY = 1 << 3, ACCESSORY = 1 << 4 }
Equippable
Each item that is equippable will have a list of features that will stay active as long as the item is equipped.
Create a new script named “Equippable.gd” in the folder “Scripts->View Model Component->Item”
The first three variables keep track of the slots of an item.
- defaultSlots – This holds the primary slot a item will equip to. A sword might have the main hand as the primary, while a shield might use the off hand slot as the primary.
- secondarySlots – Some items might have a secondary configuration to equip an item, such as a sword that can be used either single or two handed. Some weapons may also be equippable in the offhand to allow dual wielding.
- slots – This is the actual current slot an item is equipped in.
When OnEquip() is called, it loops through all the children of the main node of the item and for all Feature objects it will apply the effect to the selected stat. OnUnEquip() will do the same thing, but apply the effect in reverse to remove it.
extends Node class_name Equippable var defaultSlots:EquipSlots.Slot var secondarySlots:EquipSlots.Slot var slots:EquipSlots.Slot var _isEquipped:bool func OnEquip(): if _isEquipped: return _isEquipped = true var features:Array[Node] = self.get_parent().get_children() var filteredArray = features.filter(func(node):return node is Feature) for node in filteredArray: node.Activate(self.get_parent().get_parent().get_parent()) func OnUnEquip(): if not _isEquipped: return _isEquipped = false var features:Array[Node] = self.get_parent().get_children() var filteredArray = features.filter(func(node):return node is Feature) for node in filteredArray: node.Deactivate()
Equipment
We add an Equippable node to items that we want to be equippable, but we also need a component to add to a character to manage what is equipped.
Create a script named “Equipment.gd” in the folder “Scripts->View Model Component->Actor”
Whenever our equipment is changed here we’ll emit a signal to notify other parts of our code that it has changed. When we equip an item, we’ll call UnEquipSlots() to remove any item that has overlapping slots.
extends Node class_name Equipment signal EquippedNotification() signal UnEquippedNotification() var _items:Array[Equippable] func Equip(item:Equippable, slots:EquipSlots.Slot): UnEquipSlots(slots) _items.append(item) var itemParent:Node = item.get_parent() self.add_child(itemParent) item.slots = slots item.OnEquip() EquippedNotification.emit(self, item) func UnEquipItem(item:Equippable): item.OnUnEquip() item.slots = EquipSlots.Slot.NONE _items.erase(item) var itemParent:Node = item.get_parent() self.remove_child(itemParent) UnEquippedNotification.emit(self, item) func UnEquipSlots(slots:EquipSlots.Slot): for i in range(_items.size()-1,-1,-1): var item = _items[i] if (item.slots & slots) != EquipSlots.Slot.NONE: UnEquipItem(item)
Demo
Now that we’ve got quite a few new components, lets create another test similar to the last lesson. This time we’ll set up a mock battle with our item components that will play out in the console.
Create a new test script named “TestItems.gd” and attach it to a test Node in our scene. Just like last time, when we are done, delete or detach the script from our scene.
Should note that we won’t be creating items or characters like this once we get a few more things implemented, but this will suffice for the time being.
We create two arrays one will hold the characters for our battle, and the other will hold the items we’ll be using in our simulation. It should be noted that the characters here are still not the same ones on our battlefield. The items in the array when used will be deleted from the array. When equipped the items will similarly be deleted, but will also be added back when the item is un-equipped. We’ll keep a reference to our random number generator in _random. We don’t want to recreate this every time we need a random number. For one it would be slow, but also once we give the generator a seed value with “_random.randomize()” in _ready().
We set our simulation in motion in the _ready() function. Creating our characters and items with CreateItems() and CreateCombatants(). Once that setup is complete we call SimulateBattle()
OnEquippedItem() and OnUnEquippedItem() get called when the signal in Equipment.gd gets emitted. We set up listening to the signals when we create our hero and give it an Equipment object.
extends Node var inventory:Array[Node] = [] var combatants:Array[Node] = [] var _random = RandomNumberGenerator.new() func _ready(): _random.randomize() CreateItems() CreateCombatants() await SimulateBattle() func OnEquippedItem(eq:Equipment, item:Equippable): inventory.erase(item.get_parent()) var message:String = "{0} equipped {1}".format([eq.get_parent().name, item.get_parent().name]) print(message) func OnUnEquippedItem(eq:Equipment, item:Equippable): inventory.append(item.get_parent()) var message:String = "{0} un-equipped {1}".format([eq.get_parent().name, item.get_parent().name]) print(message) func CreateItem(title:String, type:StatTypes.Stat, amount:int): var item:Node = Node.new() item.name = title var smf:StatModifierFeature = StatModifierFeature.new() smf.name = "SMF" smf.type = type smf.amount = amount item.add_child(smf) return item func CreateConsumableItem(title:String, type:StatTypes.Stat, amount:int): var item:Node = CreateItem(title,type,amount) var consumable:Consumable = Consumable.new() consumable.name = "Consumable" item.add_child(consumable) return item func CreateEquippableItem(title:String, type:StatTypes.Stat, amount:int, slot:EquipSlots.Slot): var item:Node = CreateItem(title, type, amount) var equip:Equippable = Equippable.new() equip.name = "Equippable" equip.defaultSlots = slot item.add_child(equip) return item func CreateHero(): var actor:Node = CreateActor("Hero") var equipment = Equipment.new() equipment.name = "Equipment" equipment.EquippedNotification.connect(OnEquippedItem) equipment.UnEquippedNotification.connect(OnUnEquippedItem) actor.add_child(equipment) return actor func CreateActor(title:String): var actor:Node = Node.new() actor.name = title self.add_child(actor) var s:Stats = Stats.new() s.name = "Stats" actor.add_child(s) s.SetStat(StatTypes.Stat.MHP, _random.randi_range(500, 1000)) s.SetStat(StatTypes.Stat.HP, s.GetStat(StatTypes.Stat.MHP)) s.SetStat(StatTypes.Stat.ATK, _random.randi_range(30, 50)) s.SetStat(StatTypes.Stat.DEF, _random.randi_range(30, 50)) return actor func CreateItems(): inventory.append(CreateConsumableItem("Health Potion",StatTypes.Stat.HP, 300)) inventory.append(CreateConsumableItem("Bomb",StatTypes.Stat.HP, -150)) inventory.append(CreateEquippableItem("Sword", StatTypes.Stat.ATK, 10, EquipSlots.Slot.PRIMARY)) inventory.append(CreateEquippableItem("Broad Sword", StatTypes.Stat.ATK, 15, EquipSlots.Slot.PRIMARY | EquipSlots.Slot.SECONDARY)) inventory.append(CreateEquippableItem("Shield", StatTypes.Stat.DEF, 10, EquipSlots.Slot.SECONDARY)) func CreateCombatants(): combatants.append(CreateHero()) combatants.append(CreateActor("Monster")) func SimulateBattle(): while(VictoryCheck() == false): LogCombatants() HeroTurn() EnemyTurn() var time_in_seconds = 1 await get_tree().create_timer(time_in_seconds).timeout LogCombatants() print("Battle Completed") func HeroTurn(): var rnd:int = _random.randi_range(0, 1) match(rnd): 0: Attack(combatants[0], combatants[1]) _: UseInventory() func EnemyTurn(): Attack(combatants[1],combatants[0]) func Attack(attacker:Node, defender:Node): var s1:Stats var s2:Stats for child in attacker.get_children(): if child is Stats: s1 = child for child in defender.get_children(): if child is Stats: s2 = child var damage:int = floori((s1.GetStat(StatTypes.Stat.ATK) * 4 - s2.GetStat(StatTypes.Stat.DEF) * 2) * _random.randf_range(0.9,1.1)) s2.SetStat(StatTypes.Stat.HP, s2.GetStat(StatTypes.Stat.HP) - damage) var message:String = "{0} hits {1} for {2} damage!".format([attacker.name, defender.name, damage]) print(message) func UseInventory(): if inventory.size() == 0: print("No Inventory") return var rnd:int = _random.randi_range(0,inventory.size()-1) var item:Node = inventory[rnd] for child in item.get_children(): if child is Consumable: ConsumeItem(item) if child is Equippable: EquipItem(item) func ConsumeItem(item:Node): inventory.erase(item) var smf:StatModifierFeature var consumable:Consumable for child in item.get_children(): if child is StatModifierFeature: smf = child elif child is Consumable: consumable = child if smf.amount > 0: consumable.Consume(combatants[0]) print("Ah... a potion!") else: consumable.Consume(combatants[1]) print("Take this you stupid monster!") func EquipItem(item:Node): print("Perhaps this will help...") var toEquip:Equippable for child in item.get_children(): if child is Equippable: toEquip = child var equipment:Equipment for child in combatants[0].get_children(): if child is Equipment: equipment = child equipment.Equip(toEquip,toEquip.defaultSlots) func VictoryCheck(): for combatant in combatants: for child in combatant.get_children(): if child is Stats: if child.GetStat(StatTypes.Stat.HP) <= 0: return true return false func LogCombatants(): print("============") for combatant in combatants: LogToConsole(combatant) print("============") func LogToConsole(actor:Node): var s:Stats for child in actor.get_children(): if child is Stats: s = child var message:String = "Name:{0} HP:{1}/{2} ATK:{3} DEF:{4}".format([actor.name,s.GetStat(StatTypes.Stat.HP),s.GetStat(StatTypes.Stat.MHP),s.GetStat(StatTypes.Stat.ATK),s.GetStat(StatTypes.Stat.DEF)]) print(message)
Lets hit play and watch the battle unfold in the output window. The hero and monster each take turns performing an action. The hero can attack, use an item or equip one. The monster on the other hand will only attack on their turn. After each turn it will print out that stats for each character.
You should see the Attack and Defense stats of the hero change. In this example the hero can equip any single item, or it may equip both the sword and shield at the same time giving it +10 to both ATK and DEF from the initial stats. The broad sword uses both slots so the shield should not be able to be equipped at the same time. The Broad Sword gives +15 so the max ATK stat of the hero shouldn’t be more than the starting value plus that.
Once you’ve confirmed everything is working, you can delete or detach the test object.
Summary
As always the code from the lesson is available in the repository. We built up a lot of pieces that will be useful for our final game here. The ability to create potions and items to equip will certainly come in handy.
Looking forward to the next one!
Glad you are enjoying it. Sorry about the long delay. Next should be soon. Just put it up for review.
If you’re getting
“(INT_AS_ENUM_WITHOUT_CAST): Integer used when an enum value is expected. If this is intended cast the integer to the enum type.gdscript(30)
(INT_AS_ENUM_WITHOUT_MATCH): Cannot pass 3 as Enum “EquipSlots.Slot”: no enum member has matching value.gdscript(31)”
In TestiItems for:
inventory.append(CreateEquippableItem(“Broad Sword”, StatTypes.Stat.ATK, 15, EquipSlots.Slot.PRIMARY | EquipSlots.Slot.SECONDARY))
You can avoid the warning by creating an intermediate variable.
var combined_slots = EquipSlots.Slot.PRIMARY | EquipSlots.Slot.SECONDARY
inventory.append(CreateEquippableItem(“Broad Sword”, StatTypes.Stat.ATK, 15, combined_slots))
Anyway! Let’s see where this goes, excited for the next one! 😀
Thanks. Sorry about the long delay. Next should be up soon. Just needs review.
Been going through this over the past week or so, enjoying it! Glad to find something that’s in depth and is closer to what my eventual goal is than a lot of the other tutorials I’ve seen so far.
That was what drew me to the original Unity tutorial. There aren’t many good tutorials for Tactics RPG games out there. Basically just the original one here, and the paid Unity course by Code Monkey, although that one is a little more based on X-Com.
With a friend we want to develop an rpg tactics game and without a doubt this guide has helped me to have an idea of everything that involves developing a game, eagerly awaiting the next part.
Glad the guide is helping. Sorry about the long delay. I just sent off the next lesson for review so that should be up sometime. Good luck on the game!
Really enjoying this series, other tutorials really have a prototype feel that requires a lot of tweaking if I want to use in an actual project. You’ve also ended up with a very unique (imo) structure due to translating it from Unity, but I really like it. Very looking forward to seeing where I can take this on my own.
One thing I’m slightly concerned about is right now there doesn’t appear to be a “Base Stats” to reset to, unless I’m misunderstanding. For example, If I take an attack potion but want my attack stats to reset after the battle is over. I suppose I could make it Equippable to simulate this. Curious to hear your thoughts.
Excited for the next part regardless!
Sorry about the long delay. Lot of things taking up attention lately, but should be better for a while. Next tutorial should be up soon. I just sent it off for review.
That’s basically what you would do. As long as the modifier is attached to the character it doesn’t need to be an equippable item. The modification to the stat can then be removed when the modifier is removed.