So you can do this a number of ways, and a few have been discussed.
- Have a Dictionary that stores all the stats.
- 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 Enum “Slot”. 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: