Using Modifiers as buffs and debuffs

All Projectiles comes with a built-in method to temporarily modify your Projectiles and Attacks.

Timed Modifiers are RefCouteds used to modify objects over a period of time. You can use them to apply buffs and debuffs to your Projectiles and Attacks.


Creating and using Modifiers

As you can see here, the TimedModifier constructor can take a wide range of arguments, the most important of which are:

  • name: Modifier global identifier.
  • duration: Active time span of the modifier in seconds.
  • on_enter: Optional Callable to overwrite the set on_enter modifier property.
  • on_exit: Optional Callable to overwrite the set on_exit modifier property.
  • Working with only these parameters, we can create simple Projectile buffs:
...
func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
			projectile_manager.add_projectile_modifier(0, Projectile2DModifier.new("PROJ_BUFF", 1.0, on_projectile_buff_enter, on_projectile_buff_exit))


func on_projectile_buff_enter(_modifier: Projectile2DModifier, proj: Projectile2D) -> void:
	proj.instances += 2
	proj.proj_spread = Projectile2D.ProjectileSpread.ANGULAR
	proj.angular_spread = 90


func on_projectile_buff_exit(_modifier: Projectile2DModifier, proj: Projectile2D) -> void:
	proj.instances -= 2
	proj.proj_spread = proj.resource.proj_spread
	proj.angular_spread = proj.resource.angular_spread
  • Attack buffs:
...
func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
			projectile_manager.add_attack_modifier(0, Attack2DModifier.new("ATTACK_BUFF", 1.0, on_attack_buff_enter, on_attack_buff_exit))


func on_attack_buff_enter(_modifier: Attack2DModifier, attack: Attack2D) -> void:
	var previous_state_duration: float = attack.get_current_state_duration()

	attack.attack_charge_time *= 0.15
	attack.attack_anticipate_time *= 0.15
	attack.attack_duration_time *= 0.15
	attack.attack_recovery_time *= 0.15

	attack.current_state_lifetime += (attack.get_current_state_duration() - previous_state_duration)


func on_attack_buff_exit(_modifier: Attack2DModifier, attack: Attack2D) -> void:
	attack.attack_charge_time = attack.resource.attack_charge_time
	attack.attack_anticipate_time = attack.resource.attack_anticipate_time
	attack.attack_duration_time = attack.resource.attack_duration_time
	attack.attack_recovery_time = attack.resource.attack_recovery_time
  • Or both:
...
func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
			projectile_manager.add_projectile_modifier(0, Projectile2DModifier.new("PROJ_BUFF", 1.0, on_projectile_buff_enter, on_projectile_buff_exit))
			projectile_manager.add_attack_modifier(0, Attack2DModifier.new("ATTACK_BUFF", 1.0, on_attack_buff_enter, on_attack_buff_exit))
...

Understanding Modifier verification

By default, all your modifiers will check if the object they are targeting already has a modifier with the same name. If that is the case, the modifier will not be applied or reapplied as long as the previous one is active. Fortunately, you can modify this behavior by assigning your own custom validation method.

Here is a collection of some useful examples of verification functions:

  • Refreshable Buff
...
func _process(_delta: float) -> void:
	if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
		var destination = get_global_mouse_position()
		projectile_manager.request_execution(0, 0, position, destination)
	
	if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT):
		projectile_manager.add_projectile_modifier(0, Projectile2DModifier.new("PROJ_BUFF", 1.0, on_projectile_buff_enter, on_projectile_buff_exit, Callable(), refresh_projectile_validation))
		projectile_manager.add_attack_modifier(0, Attack2DModifier.new("ATTACK_BUFF", 1.0, on_attack_buff_enter, on_attack_buff_exit, Callable(), refresh_attack_validation))
...


func refresh_projectile_validation(_modifier: Projectile2DModifier, _proj: Projectile2D) -> bool:
	if (_proj.projectile_modifiers.has(_modifier.name)):
		_proj.projectile_modifiers[_modifier.name].lifetime = _modifier.duration
		return false
	return true


func refresh_attack_validation(_modifier: Attack2DModifier, _attack: Attack2D) -> bool:
	if (_attack.attack_modifiers.has(_modifier.name)):
		_attack.attack_modifiers[_modifier.name].lifetime = _modifier.duration
		return false
	return true
  • Stackable Buff
...
func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
			projectile_manager.add_projectile_modifier(0, Projectile2DModifier.new("PROJ_BUFF", 2.0, on_projectile_buff_enter, on_projectile_buff_exit, Callable(), stackable_projectile_validation))
			projectile_manager.add_attack_modifier(0, Attack2DModifier.new("ATTACK_BUFF", 2.0, on_attack_buff_enter, on_attack_buff_exit, Callable(), stackable_attack_validation))
...


func stackable_projectile_validation(_modifier: Projectile2DModifier, _proj: Projectile2D) -> bool:
	if !(_proj.projectile_modifiers.has(_modifier.name)):
		return true

	# I would recommend using (_modifier.stacks < _modifier.global_properties["MAX_STACKS"])
	if (_modifier.stacks < 3):
		return true
	
	return false


func stackable_attack_validation(_modifier: Attack2DModifier, _attack: Attack2D) -> bool:
	if !(_attack.attack_modifiers.has(_modifier.name)):
		return true

	# I would recommend using (_modifier.stacks < _modifier.global_properties["MAX_STACKS"])
	if (_modifier.stacks < 3):
		return true
	
	return false
  • Stackable and Refreshable Buff
...
func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
			projectile_manager.add_projectile_modifier(0, Projectile2DModifier.new("PROJ_BUFF", 2.0, on_projectile_buff_enter, on_projectile_buff_exit, Callable(), stackable_and_refreshable_projectile_validation))
			projectile_manager.add_attack_modifier(0, Attack2DModifier.new("ATTACK_BUFF", 2.0, on_attack_buff_enter, on_attack_buff_exit, Callable(), stackable_and_refreshable_attack_validation))
...


func stackable_and_refreshable_projectile_validation(_modifier: Projectile2DModifier, _proj: Projectile2D) -> bool:
	if !(_proj.projectile_modifiers.has(_modifier.name)):
		return true

	# I would recommend using (_modifier.stacks < _modifier.global_properties["MAX_STACKS"])
	if (_modifier.stacks < 3):
		return true
	
	for i: int in _modifier.copies.size():
		_modifier.copies[i].lifetime = _modifier.copies[i].duration
	return false


func stackable_and_refreshable_attack_validation(_modifier: Attack2DModifier, _attack: Attack2D) -> bool:
	if !(_attack.attack_modifiers.has(_modifier.name)):
		return true

	# I would recommend using (_modifier.stacks < _modifier.global_properties["MAX_STACKS"])
	if (_modifier.stacks < 3):
		return true
	
	for i: int in _modifier.copies.size():
		_modifier.copies[i].lifetime = _modifier.copies[i].duration
	return false
	
	return false

Integration with the Global Database

Timed Modifiers are also fully incorporated into the Global Database.

...
func _ready() -> void:
	APDatabase.set_modifier(Projectile2DModifier.new("PROJ_BUFF", 2.0, on_projectile_buff_enter, on_projectile_buff_exit, Callable(), stackable_and_refreshable_projectile_validation))
	APDatabase.set_modifier(Attack2DModifier.new("ATTACK_BUFF", 2.0, on_attack_buff_enter, on_attack_buff_exit, Callable(), stackable_and_refreshable_attack_validation))
...


func _input(event):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
			projectile_manager.add_projectile_modifier(0, APDatabase.get_projectile_2d_modifier("PROJ_BUFF"))
			projectile_manager.add_attack_modifier(0, APDatabase.get_attack_2d_modifier("ATTACK_BUFF"))