Implements modifier system for weapons

Adds a modifier system allowing dynamic modification of weapon
stats and behavior. This includes:

- Creating ModifierLibrary to manage available modifiers.
- Adds ModifierManager to handle equipping and unequipping modifiers
- Adds a new RangedWeaponComponent to handle firing projectiles and
  managing modifiers.
- Introduces a DebugUI for in-game modifier management.
- Introduces an "Unlimited Power" modifier that changes the projectile scene.
- Modifies stats components to work with the new modifier system.

This system allows for more flexible and customizable weapon
functionality.
This commit is contained in:
Dan Baker 2025-05-08 18:31:19 +01:00
parent 9f66ab0a73
commit 70839387ca
22 changed files with 432 additions and 40 deletions

View file

@ -18,6 +18,12 @@ func _ready() -> void:
func set_stats(stats: StatsComponent) -> void:
self.stats_component = stats
func has_modifier(modifier_id: String) -> bool:
for modifier in modifiers:
if modifier.id == modifier_id:
return true
return false
func add_modifier(modifier: Modifier) -> void:
modifiers.append(modifier)
modifier.on_equip(get_parent())
@ -63,6 +69,29 @@ func recalculate_stats() -> void:
emit_signal("stats_updated")
func check_callable(func_name: String) -> Callable:
# Create a default callable that does nothing and returns null
var default_callable = func(): return null
# Check each modifier in priority order
for modifier in modifiers:
if modifier.has_method(func_name):
# Return the callable from this modifier
return Callable(modifier, func_name)
# Return the default callable if no modifier has the function
return default_callable
# Convenience method to check and call in one step
func check_and_call(func_name: String, args := []):
var callable = check_callable(func_name)
if callable.is_valid():
if args.size() > 0:
return callable.callv(args)
else:
return callable.call()
return null
func _apply_modifier_stats(modifier: Modifier) -> void:
if modifier.has_method("apply_stats_modification"):
modifier.apply_stats_modification(stats_component)

View file

@ -5,7 +5,7 @@ class_name FireRateAdditive extends Modifier
func _init():
id = "fire_rate_additive"
display_name = "Rapid Fire"
description = "Increases fire rate by %0.1f shots per second" % fire_rate_bonus
description = "Increases fire rate by %0.1f shots per round" % fire_rate_bonus
modifier_type = ModifierType.ADDITIVE
func apply_stats_modification(stats: StatsComponent) -> void:

View file

@ -3,11 +3,10 @@ class_name FireRateMultiplicative extends Modifier
@export var fire_rate_multiplier: float = 1.2 # 20% faster firing
func _init():
id = "fire_rate_multiplicative"
display_name = "Frenzy"
description = "Increases fire rate by %d%%" % ((fire_rate_multiplier - 1.0) * 100)
modifier_type = ModifierType.MULTIPLICATIVE
id = "fire_rate_multiplicative"
display_name = "Frenzy"
description = "Increases fire rate by %d%%" % ((fire_rate_multiplier - 1.0) * 100)
modifier_type = ModifierType.MULTIPLICATIVE
func apply_stats_modification(final_stats: Dictionary, _base_stats: Dictionary) -> void:
if final_stats.has("fire_rate"):
final_stats.fire_rate *= fire_rate_multiplier
func apply_stats_modification(stats: StatsComponent) -> void:
stats.update_stat("ranged.attack_rate", stats.get_stat("ranged.attack_rate") * fire_rate_multiplier)

View file

@ -17,15 +17,15 @@ enum ModifierType {
# Called when the modifier is added to a weapon or ability
func on_equip(_owner) -> void:
pass
pass
# Called when the modifier is removed
func on_unequip(_owner) -> void:
pass
pass
# Override in child classes for specific modification logic
func modify_projectile(_projectile) -> void:
pass
pass
func modify_ability(_ability) -> void:
pass
pass

View file

@ -3,12 +3,11 @@ class_name ProjectileSizeMultiplicative extends Modifier
@export var size_multiplier: float = 1.5 # 50% bigger
func _init():
id = "size_multiplicative"
display_name = "Giant Projectiles"
description = "Multiplies projectile size by %0.1fx" % size_multiplier
modifier_type = ModifierType.MULTIPLICATIVE
priority = 10 # Higher priority than the additive version
id = "size_multiplicative"
display_name = "Giant Projectiles"
description = "Multiplies projectile size by %0.1fx" % size_multiplier
modifier_type = ModifierType.MULTIPLICATIVE
priority = 10 # Higher priority than the additive version
func apply_stats_modification(final_stats: Dictionary, base_stats: Dictionary) -> void:
if final_stats.has("projectile_size"):
final_stats.projectile_size *= size_multiplier
func apply_stats_modification(stats: StatsComponent) -> void:
stats.update_stat("ranged.projectile_size", stats.get_stat("ranged.projectile_size") * size_multiplier)

View file

@ -0,0 +1,17 @@
class_name UnlimitedPower extends Modifier
@export var fire_rate_bonus: float = 1.0 # +1 shot per second
@export var ranged_damage: float = 99.5
func _init():
id = "unlimited_power"
display_name = "Unlimited Power"
description = "Shoot lightning bolts. Fire rate + %0.1f. Ranged damage + %0.1f" % [fire_rate_bonus, ranged_damage]
modifier_type = ModifierType.ADDITIVE
func apply_stats_modification(stats: StatsComponent) -> void:
stats.update_stat("ranged.attack_rate", stats.get_stat("ranged.attack_rate") + fire_rate_bonus)
stats.update_stat("ranged.damage", stats.get_stat("ranged.damage") + ranged_damage)
func set_projectile_scene(weapon: RangedWeaponComponent) -> void:
weapon.projectile_scene = preload("res://assets/projectiles/projectile_lightning.tscn")

View file

@ -0,0 +1 @@
uid://b21isiwfqrjyi

View file

@ -3,15 +3,68 @@ extends Node2D
class_name RangedWeaponComponent
@export var stats: StatsComponent
@export var basic_projectile: PackedScene = preload("res://assets/projectiles/basic_projectile.tscn")
@onready var modifier_manager = $ModifierManager
@onready var projectile_scene: PackedScene = basic_projectile
var can_fire: bool = true
var cooldown: Timer
func _init() -> void:
cooldown = Timer.new()
add_child(cooldown)
cooldown.one_shot = true
func _ready() -> void:
Log.pr("RangedWeaponComponent initialized")
modifier_manager.set_stats(stats)
modifier_manager.connect("modifier_added", _on_modifier_added)
modifier_manager.connect("modifier_removed", _on_modifier_removed)
cooldown.connect("timeout", _on_fire_timer_timeout)
func add_modifier(modifier: Modifier) -> void:
modifier_manager.add_modifier(modifier)
func remove_modifier(modifier_id: String) -> void:
modifier_manager.remove_modifier(modifier_id)
modifier_manager.remove_modifier(modifier_id)
func fire(direction: Vector2, target_position: Vector2) -> void:
if !can_fire:
return
spawn_projectile(global_position, direction, target_position)
can_fire = false
cooldown.start(1 / stats.get_stat("ranged.attack_rate"))
func set_projectile_scene() -> void:
projectile_scene = basic_projectile
modifier_manager.check_and_call("set_projectile_scene", [self])
func spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2, target_position: Vector2) -> void:
Log.pr("Spawning projectile")
modifier_manager.check_and_call("spawn_projectile", [self])
## TODO: Handle multiple shots per fire
var projectile = projectile_scene.instantiate()
projectile.global_position = spawn_position
projectile.target_position = target_position
projectile.speed = 200 # stats.get_stat("projectile_speed")
projectile.damage = 10 # stats.get_stat("damage")
projectile.lifetime = 2 # stats.get_stat("projectile_lifetime")
projectile.source_weapon = self
var size = stats.get_stat("ranged.projectile_size")
projectile.set_projectile_scale(Vector2(size, size))
if get_tree() and get_tree().get_root():
get_tree().get_root().add_child(projectile)
func _on_modifier_added(_modifier: Modifier) -> void:
set_projectile_scene()
func _on_modifier_removed(_modifier: Modifier) -> void:
set_projectile_scene()
func _on_fire_timer_timeout() -> void:
can_fire = true