So there are two ways to apply percentage bonuses. The first is to add them. The second is to multiply them.
So, taking a bonus of 10% and 20%, let’s see the differences.
Adding Percentage Bonuses
First Bonus
- Your starting bonus is 100%.
- You gain a bonus multiplier of 10%.
- 1.00 + 0.10 = 1.10
- Your new bonus is 110%.
Second Bonus
- Your bonus is 110%.
- You gain a bonus multiplier of 20%.
- 1.10 + 0.20 = 1.30
- Your new bonus is 130%
Multiplying Percentage Bonuses
First Bonus
- Your starting bonus is 100%.
- You gain a bonus multiplier of 10%.
- 1.00 * 0.10 = 0.10 or %10
- 1.00 + 0.10 = 1.10
- Your new bonus is 110%. (Same result as above.)
Second Bonus
- Your bonus is 110%.
- You gain a bonus multiplier of 20%.
- 1.10 * 0.20 = 0.22 or 22%
- 1.10 + 0.22 = 1.32
- Your new bonus is 132% (Slightly higher than above.)
Which approach are you wanting to take?
If you are adding, you need to track the total in a separate value. If you are multiplying, you can just do the math.
As for where to track it, track it in the component. It shouldn’t exist at all in the PlayerBasicStatsData object, because you are factoring it out of there and placing it into a component.
Having said that, naming something “Component” is a surefire way to mess up your thinking when trying to figure these things out. So let’s change the name from ExpCollectorComponent to ExperienceCollector. Now, it’s very clear that this thing’s job is to handle collecting expereience.
class_name ExperienceCollector extends Area2D
@export var base_radius: float = 80.0
@export var max_radius: float = 1000.0
@export var radius_multiplier: float = 1.0:
set(value):
radius_multiplier = value
exp_collector_area.shape.radius = clampf(base_radius * radius_multiplier, 0.0, max_radius)
@onready var exp_collector_area: CollisionShape2D = %ExpCollectorArea
Whenever you change the radius_multiplier, the radius gets set, but never above the max_radius or below 0.0. You don’t need a separate value to store that information. It’s already stored in the Shape.
All three of those values should come out of PlayerBasicStatsData, because you don’t need them.
A note on naming: When you are naming classes, functions and variables, think about how you will use them. ExpCollectorComponent.max_exp_collection_radius is very verbose. ExperienceCollector.max_radius communicates the same information, but is easier to read. Naming is very important, not just for readability, but can also affect the way you think about the functionality of a class/object.
My next suggestions would be to consider storing experience points in the ExperienceCollector and consider naming it just Experience.
class_name Experience extends Area2D
@export var base_radius: float = 80.0
@export var max_radius: float = 1000.0
@export var radius_multiplier: float = 1.0:
set(value):
radius_multiplier = value
exp_collector_area.shape.radius = clampf(base_radius * radius_multiplier, 0.0, max_radius)
@onready var exp_collector_area: CollisionShape2D = %ExpCollectorArea
var points: float = 0.0
Then you can reference it in your Player:
class_name Player extends CharacterBody2D
@onready var experience: Experience = %Experience
func _ready() -> void:
print(experience.points)