How to approach modifiable item stats

Godot Version

4.6 stable

Question

I would like to make a character equipment system where items themselves can have upgrades slotted into them, sort of like “gem slots” in some games. For now, I’m just trying to figure out numerical stat upgrades, not any more complex effects.

So, for example, some item has a “defense” stat (or “modifier”) that, when equipped, gives a character +5 defense.

Then, there is a special type of item, like the aforementioned gems, which can be equipped to items like that to temporarily increase the defense buff until unequipped. In the example above, there could be a gem that specifically looks for some “defense” stat, then increases it by +3, so when a character equips it, it gives a total of +8. I’d like percent modifiers to be possible too, so there could be a gem that gives a +10% bonus, for example.

I have read lots of different articles and tutorials on stat/modifier systems, and I can implement the basics there. I know, for example, that I’d like characters to have a dictionary of stats. Then there can be a “Modifier” class which holds data about its value and math operation, and such Modifiers are used to calculate final stat values. I’ve implemented such a system before to some extent, but I’m still pretty inexperienced.

To achieve what I’m talking about above… I have a few ideas, but none of them feel very good, and I haven’t been able to find much online about it. It seems like I’ll basically need to make the Modifiers… modifiable.

How would you approach this idea?

Sounds like you want to use the abstract factory design pattern along with the strategy design pattern.

Basically you want an abstract class with which you instantiate (factory pattern) your upgradable modifiers; and make sure you can serialize/save them. You keep those modifiers in a array/list (or lists) and walk through them whenever you to re-calculate (strategy pattern) the stats of your player/weapon/npc. The trick is not to do this on every tick. For upgrading those modifiers, that’s the simple part really.

In the both you might want to go through the list of existing modifiers, you know, what’s a +1 sword modifier worth in the hands of a bard…

Keep the system as simple as possible.

Make a modifiers resource:

class_name Modifiers
extends Resource

@export var damage_bonus: float  # points
@export var damage_multiplier: float  # percents
@export var armor_bonus: float
@export var speed_bonus: float
@export var poison_resistance: float
...

Then add the modifiers to your items and gems:

class_name Item
extends Node # or whatever

@export var modifiers: Modifiers

#-----------

class_name Gem
extends Node # or whatever

@export var modifiers: Modifiers

When the player’s gear changes (any item equipped/unequipped, any gem added/removed), just recalculate player’s damage, armor and whatever from ground up. For example, total damage calculation:

damage = 0
iterate items:
    sword, damage += 10 
        gem in sword, damage += 5
        another gem in sword, damage += 2
    magic ring, doesn't have damage bonus 
        gem in ring, damage += 1 

--> total damage = 18

If you have a huge number of modifiers, you can split the Modifiers resource to smaller pices, for example separate weapon modifiers, armor modifiers and so on.

For upgrading those modifiers, that’s the simple part really.

This is actually the part I’m not sure about, haha.

Say I have a class called Stat that might have stuff like this:

var base_value: float
var current_value: float
var modifiers: Array[Modifier]

A Modifier could look like:

var value: float
var operation: operations //enum value

Then, characters have a Dictionary[String, Stat]. There is a method somewhere to recalculate a Stat based on all the Modifiers it currently holds, only when needed. Maybe it’s in Stat, maybe it’s in some Stat container component.

Anyway, I assumed that items would have these Modifiers on them, maybe in a Dictionary[String, Modifier] where the String is the target Stat key for it to go on. Then when you equip an item you just iterate over the Modifiers and apply them to the right Stats.

But then I have this design goal that messes with that plan. Now I want to add a gems with their own Modifiers to apply to item Modifiers. It wouldn’t work with the system above.

My first idea was inheritance. Modifier extends Stat. Seemed okay at first, but then I got worrying about how complicated it started to feel when I added things like separate init functions, signals, min and max values, etc… and I just wondered if there were a simpler way. If not, I guess this could theoretically work.

My second idea was to use composition and make the Modifier “value” a Stat. But it still felt a little cumbersome, always having to make a Stat when making a Modifier, and accessing the value like modifier.value.current_value…

So, I’m not necessarily married to the entire Stat class concept above, either. Maybe there is a more elegant way if it was a Dictionary[String, float] and we stored the current values in another Dictionary or something (to avoid calculating them all the time, as you said, because this is a real-time combat game… so I want to avoid recalculating every time)

I guess what I mean to say is, I’ve got the whole “iterating over modifiers to apply them and only recalculating stats when necessary” idea down, but it’s more the entire class structure that I’m struggling with. What should be a class, what should reference what, and all that.

If you have an equipment system for your player, could you not just modify it slightly and attach it to the equipments themselves?

Hmm, yeah, I thought about something like this, vaguely. For me, I’d want a Dictionary of those Modifiers in my case to more freely create them, but that would be simple. In that case, maybe I wouldn’t even really need a class for it.

I am currently processing your answer, but I think this has potential. Maybe I can play with the ideas here. I’m not currently sure how to respond, haha. This is a totally valid solution, just wondering how applicable it is for me specifically. I’ll keep it in mind going forward. Thank you!

Yeah, this is basically what I am thinking of doing, I guess it’s just more of a “how” since I’m kind of inexperienced still. I’m wondering if I actually have to rethink my equipment system in order to keep it simple.

Currently, my equipment system is literally just… Characters have a Dictionary[String, Stat] for keeping all their stats (the Stat object just holds base/current values and an array of Modifiers). When equipping an item, items have a Dictionary[String, Modifier], where the keys are the Stat keys, so we just iterate over that and add them to the respective Stats.

So then to do the same thing with items… they would also need to have a Dictionary[String, Stat]… and then we would somehow need to make Modifiers out of those… and then update them whenever the associated Stats changed… and I’m just not sure about that. As I said, I’m kind of a newbie, so this is starting to feel a little complicated and I felt like I was going in the wrong direction. I feel like I have the actual coding skills, but not the problem-solving ones yet, lol.

(Note: I’m gonna come back to this much later, it is late where I am and I’ll be busy tomorrow, but thank you everyone so far!)

If it is to any help, i could share my solution. I’ve tried to make it modular and hopefully you should be able to just paste it onto any of your items and enter a few strings and ints in the editor.

Not sure if mine will suit you though.

In my opinion, this is a bad idea:

var base_value: float
var current_value: float
var modifiers: Array[Modifier]

Also an actual class for a single stat feels like a bad idea. And by a “bad idea” I mean overcomplicated. This looks like you are collecting all modifiers from items and gems to the player’s stat class. That’s gonna be a very hard to maintain and it’s probably useless too. Let the items and gems have their modifiers. When recalculating the stat’s current value, its much easier to iterate through everything (items, gems, temporary buffs) that can contain modifiers than to try keeping all the modifiers in one array.

I m o it would make sense to only iterate over the item stats whenever a modifier is added or removed, and keep separate base (exported base stats string, int) and combined base + mods (not exported) dictionaries, and only iterate over combined whenever recalculating stats.

Any thoughts on that solution?

So you can do this a number of ways, and a few have been discussed.

  1. Have a Dictionary that stores all the stats.
  2. Have Resources that store all the stats and add them as variables.

I think it helps to ask what you want the whole thing to look like? So let’s say this is a 3D game, and I want to drag and drop gems onto a sword, and that modifies the sword’s properties. I also physically want to show the slots so that the player can customize the look of their sword. In the original description I immediately thought of FF7 and Materia. This is what I am envisioning based on what I read.

So each Gem has a modifier that is equipped to an Item, then when worn/wielded the Item applies all its modifiers to the Player.

Additionally, the OP wants all of the modifiers stored in maybe a Dictionary form. Additionally we have these assumptions from what I’ve read:

  • A bonus is always stored as a float.
  • A modifier can either apply a straight value (+3) or a percentage (+10%).
  • A Gem only stores a single modifier.
  • An Item can have multiple Gems.
  • A Player can have multiple Items.

Godot is all about signals. When stuff happens, you can listen to that signal and make something happen. So, regardless of whether a Gem or Item is a Dictionary, Resource, Node or Scene, there are ways to tell when it is added, and when it is removed from something.

So my first suggestion for your architecture is to plan to pay attention to those signals, and use those to modify your stats. Then you don’t have to traverse Dictionaries every time you want to calculate a Stat.

Somewhere, each Stat needs to be hardcoded. You have a finite amount of them, and they affect things in gameplay or they don’t need to be there. Since I don’t know what Stat objects you want, I’m going to use the old standby: D&D.

Attributes

Base + Ancestry Modifier + Misc Modifiers

  • Strength (STR)
  • Dexterity (DEX)
  • Constitution (CON)
  • Intelligence (INT)
  • Wisdom (WIS)
  • Charisma (CHA)

Other Stats

  • Proficiency Bonus (PROF) 2 + 1 per 4 levels
  • Hit Dice (HD) - 1 per level
  • Hit Points (hp) - HD + CON Modifier
  • Armor Class (AC) 10 + Armor Bonus + DEX Modifier + Magic Bonuses
  • Attack (ATK) Proficiency Bonus + STR Modifier + Magic Bonuses

Sword and Gem Combo

That’s a lot of combinations. So let’s take a Sword (Item) with a +1 Gem equipped to a Level 1 character with a 14 STR (+2 Bonus). Our attack bonus is:

Base + Proficiency Bonus + STR Bonus + Gem
0 + 2 + 2 + 1 = +5

Gem Design

Let’s design our gem first. It needs a visual representation, so a Texture2D. It should have a name. We need to know what it is modifying (Attack), whether its a bonus or percentage, and the actual bonus.

What it is modifying is a finite number of things. Our list of attributes and other stats from above. So that can be an Enum.

We need to know whether it’s a straight bonus or percentage. Now we could store a single float and a boolean, but that doesn’t take up less space in RAM than two float, or a float and an int, because of the way variables are stored in Godot. If we just store both numbers, and set the one we want to use - it’s the same thing. Plus, it means if we want to get fancy later and give a Gem that has a +3 bonus AND a +10% bonus, we don’t have to change anything but the numbers of the Gem. So I am going to choose an int for the straight bonus, because in D&D bonuses are never fractions, and we can use modulo to figure bonuses. And the percentage we will make a float.

Attributes Enum

This is something we want to use globally. So we are going to use the fact that we can create a RefCounted object with a class_name and then reference an Enum inside it globally.

attribute.gd

class_name Attribute

enum Type {
	STRENGTH,
	DEXTERITY,
	CONSTITUTION,
	INTELLIGENCE,
	WISDOM,
	CHARISMA,
	PROFICIENCY_BONUS,
	HIT_DICE,
	HIT_POINTS,
	ARMOR_CLASS,
	ATTACK,
}

This is the entire file. It doesn’t extend anything, which means it defaults to extending a RefCounted. (We could have it extend Object instead, but that only saves us 8 bytes of RAM.)

You’ll note that I’ve decided to call everything an attribute, and that the file name and class name are not plural. - even though there is more than one. This is because when we type Attribute.Type.STRENGTH it will be human readable, and Attributes.Type.STRENGTH would look weird.

Gem Resource

We are now ready to make our Gem and for now it’s going to be a Resource.

gem.gd

class_name Gem extends Resource

@export var name: String
@export var texture: Texture2D
@export var modified_attribute: Attribute.Type
@export var modifier: int
@export_range(-100.0, 100.0, 1.0, "%") var percentage_modifier: float

It’s going to look like this in the Inspector:

Our Enum dropdown look good. Our percentage_modifier can be anywhere from -100% to 100% in 1% increments. (Negative because we might want to make cursed gems.) Once we’ve finished editing it, we can save it to disk for use in multiple places.

Sword Design

Our Sword is of type Item. It’s probably of type Weapon. So let’s take explore that.

An Item has a variable number of gems it can contain. We can probably represent that as an Array. It also needs a name and a texture. Finally, we probably should identify what Slot on a Player an Item can be equipped into. This will be finite, so we can make an Enum for that too.

A Weapon is an Item that can only be equipped in a certain Slot. It also needs to deal damage. Since this is D&D it needs to deal variable damage, so we need a minimum and maximum damage. Ultimately, it will probably also need an in-game representation with animations. So instead of just a Texture2D, we will also give it a PackedScene which can be used to reference whatever needs to be instantiated in the game.

While a Sword is a type of weapon, it may not need its own object. It’s just a Weapon. For now, we are going to stick to Resources still. We need an Item and a Weapon Resource.

Inventory Slot Enum

We need a place to equip all those items. So again, we will make an RefCounted called Inventory, and name the EnumSlot”. Later, we may wish to make Inventory an actual Node, like a Control or Node2D. We can do that by adding an inheritance and the Enum will still continue to work even if we add code and change what this class does later.

inventory.gd

class_name Inventory

enum Slot {
	HEAD,
	WEAPON,
	SHIELD,
	RING,
	AMULET,
	HAND,
	TORSO,
	LEGS,
	FEET,
}

Item Resource

This really will likely never get used other than as a base for more complex items, but it can store all the things they all have in common.

item.gd

class_name Item extends Resource

@export var name: String
@export var texture: Texture2D
@export var slot: Inventory.Slot
@export var gems: Array[Gem]

Weapon Resource

Now we add the weapon code, create a Weapon - then populate, and save it.

class_name Weapon extends Item

@export var minimum_damage: int
@export var maximum_damage: int
@export var weapon_scene: PackedScene

Slot Resource

We now need slots to add to the Player. They need to only allow valid items to be added to them - so they have two fields: the type of slot they allow, and a place to store the Item.

slot.gd

class_name Slot extends Resource

signal item_added(item: Item)
signal item_removed(item: Item)

@export var type: Inventory.Slot
@export var item: Item:
	set(value):
		if value == null:
			item_removed.emit(item)
			item = value
			return
		if item.slot != type:
			return
		item = value
		item_added.emit(item)

Here we are using a setter for the item variable. It will accept any Item object, or derived object like a Weapon. But only if the Item’s slot matches the Slot’s type - which are both of type Inventory.Slot.

We also are adding generic signals to the Slot that the player can connect to. We emit them if the item is deleted or successfully changed.

Player (CharacterBody2D)

Last step is making the player and hooking in the stats. I don’t know if this is a 2D or 3D game, so I’m going to pick a CharacterBody2D (and its default script) to build on. We are going to add to the Player the attributes we need, and a weapon_slot and tie some (a lot of) code to it all.

class_name Player extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0

@export var level: int = 1:
	set(value):
		level = value
		@warning_ignore("integer_division")
		proficiency_bonus = 2 + (level/4)
@export var strength: int = 10:
	set(value):
		strength = value
		if strength >= 10:
			@warning_ignore("integer_division")
			strength_bonus = (strength - 10) / 2
		else:
			@warning_ignore("integer_division")
			strength_bonus = (10 - strength) / 2
@export var weapon_slot: Slot

var strength_bonus: int:
	set(value):
		strength_bonus = value
		attack += 0
var proficiency_bonus: int:
	set(value):
		proficiency_bonus = value
		attack += 0
var attack: int = 0:
	set(value):
		attack = attack - strength_bonus - proficiency_bonus #Remove the built-in bonuses
		attack += value - attack
		attack = attack + strength_bonus + proficiency_bonus #Add back the built-in bonuses


func _ready() -> void:
	weapon_slot.item_added.connect(_on_item_added)
	weapon_slot.item_removed.connect(_on_item_removed)


func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	# Handle jump.
	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()


func _on_item_added(item: Item) -> void:
	for gem in item.gems:
		_add_equipment_modifier(gem.modified_attribute, gem.modifier, gem.percentage_modifier)


func _on_item_removed(item: Item) -> void:
	for gem in item.gems:
		_remove_equipment_modifier(gem.modified_attribute, gem.modifier, gem.percentage_modifier)


func _add_equipment_modifier(attribute: Attribute.Type, modifier: int = 0, percentage_modifier: float = 0.0) -> void:
	match(attribute):
		Attribute.Type.ATTACK:
			attack += modifier
			@warning_ignore("narrowing_conversion")
			attack *= 1.0 + (percentage_modifier/100)


func _remove_equipment_modifier(attribute: Attribute.Type, modifier: int = 0, percentage_modifier: float = 0.0) -> void:
	match(attribute):
		Attribute.Type.ATTACK:
			@warning_ignore("narrowing_conversion")
			attack /= 1.0 - (percentage_modifier/100)
			attack -= modifier

Basically, since we cannot tell when something is added or removed from an Array, we have to have an entry point to know when something is added, and then iterate through any Array objects we have. Since we have to have a weapon Slot for Weapon objects, we can connect to all the slot’s signals the same way, and direct them to the same functions. There, we parse all the gems and either add or remove the values from the correct attribute based on a match statement.

The attack value recalculates itself whenever that happens. By using cascading setters, we can also reset the value whenever strength or level changes.

(There may be some issues with removing the percentages correctly, but it’s late and I didn’t test it.)

Conclusion

This is just one way to do it. I decided to put it together for fun because it’s been in the back of my mind to build something like this, and this was my excuse. I walked you through my reasoning, because I think there’s a LOT to think about to make something this interconnected. (I had to add a few things to slot.gd as I was working on player.gd.) I really just wanted to showcase that. I also think building a prototype is worth a lot of discussion. There’s going to be refactors. A few I can think of that might come up:

  • Trying to shove all attributes into Dictionaries, and replacing the Enums with Strings. I don’t suggest this, but you could do it - if you don’t like those drop-downs.
  • Changing it so you can have multiple attribute changes per Gem. Since you’re already having to scan the Gems, you could go a bit deeper, and have an Array[Modifier] variable on each Gem and store multiple modifiers in the Gem.
  • You could modify the Slot object to take multiple types. For example if you wanted a Right Hand slot, and the Player could put a Weapon or Shield in it.
  • You could modify an Item to require multiple Slots, like a bow or two-handed sword. Or a full suit of armor that took up the Head, Hand, Torso, Legs* and Feet slots.

In the end, maybe this is just a long answer to @baba asking:

5 Likes

I am in awe. I sit back down at my computer when I get some free time and this is what I come back to, haha.

Not only did you absolutely go above and beyond with this question, and not only did you present all of it super well, but you actually unwittingly touched on a few topics directly relevant to my game that I would have needed to ask different questions for. Absolutely amazing, I can’t thank you enough for taking the time to type all this out and make a prototype. Even if it was something you’d been meaning to do for a while.

While it’s definitely not exactly like my game (though I do love some D&D), I am totally confident that I can take some concepts here and rework them to fit what I need. I was primarily just having trouble envisioning the way forward – and here you came and illustrated it for me. I was getting confused by how to connect all the different components, and what to even make into a component, but with this, I now have a much better idea of it all!

To touch on the Dictionary thing, the whole reason I want Dictionaries is because I don’t actually plan to have a finite amount of stats. It’s a game with multiple characters that I want to keep adding onto, and every new character will have their own unique stats with their own unique uses, min and max values, etc., so what I’m envisioning is that my Resource that the Character class reads from will just have an open Dictionary for that. I was already using a global enum, as you covered, for dropdowns like the above, and planning to add to it with every new stat.

The major pain point for me was, if my Items had Modifiers, how I could add something like Gems to change those Modifiers and display the final stats and such in the UI… but I think I see now that there is no need to change the actual Modifiers. For example, I guess I could use signals when equipping an Item or Gem, and create a function specific to the Attack stat to directly recalculate its value based on the equipment. Rather than using a “Stat” class.

So yes, @baba and @Dizzy_Caterpillar were both getting at this too. Thank you for your help! I hope this thread can be of use to anyone finding it in the future, too. I will be getting back to the drawing board!

2 Likes

Glad it helped. :slight_smile: I had another thought.

You could turn the Attribute class into a Node, and hang each one off the Player based on the stats you want. I give an example of doing so with Health and Hunger components in this thread: Am I doing Components/composition right? - #3 by dragonforge-dev

I think that would help you to encapsulate the code more, and pull a lot out of the Player script. And you could make it generic the way the Weapon script is. Then you could dynamically add them at runtime pulling from your attribute Dictionary. My example had a lot of interdependencies that you may not have, which might make this similar.

1 Like

Definitely the most put effort into post I’ve read on this forum! Very helpful and would even make a good tutorial i m o!

1 Like

Thanks. It’s not even my longest one. You should check out the one I did on Health and Hunger components I linked just above. My longest post was the other day and @normalized gave me crap about it but I don’t even remember what topic it was on. :slight_smile:

I have considered trying to put some of these on a webpage somewhere. I just haven’t. Before iosXCoder left the forum, he was encouraging me to do something with GDQuest’s Learn GDScript project and make my own stuff, but it was in 3.x and I didn’t want to port it. I’ve thought about making my own.