Refactors modifier system to use StatsComponent

Moves modifier logic to utilize a central StatsComponent for managing and applying stat modifications.

This change centralizes stat management and simplifies the application of modifiers, enhancing code maintainability and reducing redundancy.
It also moves modifier files to the correct directory.
This commit is contained in:
Dan Baker 2025-05-07 15:08:11 +01:00
parent 19cc8cb573
commit 9f66ab0a73
21 changed files with 135 additions and 97 deletions

View file

@ -1,28 +1,22 @@
@icon("res://assets/editor/64x64/fc1098.png") @icon("res://assets/editor/64x64/fc1098.png")
extends Node2D extends Node2D
class_name ModifierManagerTwo class_name ModifierManager
signal modifier_added(modifier) signal modifier_added(modifier)
signal modifier_removed(modifier) signal modifier_removed(modifier)
signal stats_updated() signal stats_updated()
var stats: StatsComponent var stats_component: StatsComponent
# Stores all active modifiers # Stores all active modifiers
var modifiers: Array[Modifier] = [] var modifiers: Array[Modifier] = []
# Base stats (before modifiers)
var base_stats: Dictionary = {}
# Final calculated stats
var final_stats: Dictionary = {}
func _ready() -> void: func _ready() -> void:
Log.pr("ModifierManager initialized") Log.pr("ModifierManager initialized")
Log.pr("Stats: ", stats) Log.pr("StatsComponent: ", stats_component)
func set_stats(stats_component: StatsComponent) -> void: func set_stats(stats: StatsComponent) -> void:
self.stats = stats_component self.stats_component = stats
func add_modifier(modifier: Modifier) -> void: func add_modifier(modifier: Modifier) -> void:
modifiers.append(modifier) modifiers.append(modifier)
@ -42,7 +36,7 @@ func remove_modifier(modifier_id: String) -> void:
func recalculate_stats() -> void: func recalculate_stats() -> void:
# Reset stats to base values # Reset stats to base values
final_stats = base_stats.duplicate() stats_component.reset_stats()
# Sort modifiers by priority # Sort modifiers by priority
modifiers.sort_custom(func(a, b): return a.priority > b.priority) modifiers.sort_custom(func(a, b): return a.priority > b.priority)
@ -67,26 +61,8 @@ func recalculate_stats() -> void:
if modifier.modifier_type == Modifier.ModifierType.CONDITIONAL: if modifier.modifier_type == Modifier.ModifierType.CONDITIONAL:
_apply_modifier_stats(modifier) _apply_modifier_stats(modifier)
# Apply caps and floors to stats
_apply_stat_limits()
emit_signal("stats_updated") emit_signal("stats_updated")
func _apply_modifier_stats(modifier: Modifier) -> void: func _apply_modifier_stats(modifier: Modifier) -> void:
if modifier.has_method("apply_stats_modification"): if modifier.has_method("apply_stats_modification"):
modifier.apply_stats_modification(final_stats, base_stats) modifier.apply_stats_modification(stats_component)
func _apply_stat_limits() -> void:
pass
# Example: Cap fire rate
#if final_stats.has("fire_rate"):
# final_stats.fire_rate = min(final_stats.fire_rate, 20.0) # Max 20 shots per second
# final_stats.fire_rate = max(final_stats.fire_rate, 0.5) # Min 0.5 shots per second
# Example: Cap projectile size
#if final_stats.has("projectile_size"):
# final_stats.projectile_size = min(final_stats.projectile_size, 5.0) # Max 5x normal size
# final_stats.projectile_size = max(final_stats.projectile_size, 0.2) # Min 0.2x normal size
func get_stat(stat_name: String, default_value = 0):
return final_stats.get(stat_name, default_value)

View file

@ -0,0 +1,12 @@
class_name FireRateAdditive extends Modifier
@export var fire_rate_bonus: float = 1.0 # +1 shot per second
func _init():
id = "fire_rate_additive"
display_name = "Rapid Fire"
description = "Increases fire rate by %0.1f shots per second" % fire_rate_bonus
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)

View file

@ -0,0 +1,21 @@
class_name ProjectileSizeAdditive extends Modifier
@export var size_increase: float = 0.5 # +50% bigger
func _init():
id = "size_additive"
display_name = "Enlarged Projectiles"
description = "Increases projectile size by %d%%" % (size_increase * 100)
modifier_type = ModifierType.ADDITIVE
func apply_stats_modification(stats: StatsComponent) -> void:
stats.update_stat("ranged.projectile_size", stats.get_stat("ranged.projectile_size") + size_increase)
func modify_projectile(_projectile) -> void:
pass
# This will be called when the projectile is created
# Scale is often handled in the recalculate_stats method, but we can also add visual effects here
#projectile.connect("on_spawned", _on_projectile_spawned)
func _on_projectile_spawned(_projectile) -> void:
pass

View file

@ -9,3 +9,9 @@ class_name RangedWeaponComponent
func _ready() -> void: func _ready() -> void:
Log.pr("RangedWeaponComponent initialized") Log.pr("RangedWeaponComponent initialized")
modifier_manager.set_stats(stats) modifier_manager.set_stats(stats)
func add_modifier(modifier: Modifier) -> void:
modifier_manager.add_modifier(modifier)
func remove_modifier(modifier_id: String) -> void:
modifier_manager.remove_modifier(modifier_id)

View file

@ -2,7 +2,7 @@
extends Node2D extends Node2D
class_name StatsComponent class_name StatsComponent
var stats: Dictionary[String, Variant] = { var base_stats: Dictionary[String, Variant] = {
"base": { "base": {
"health": 100, "health": 100,
"max_health": 100, "max_health": 100,
@ -30,3 +30,66 @@ var stats: Dictionary[String, Variant] = {
"pierce_count": 0 "pierce_count": 0
} }
} }
var stats: Dictionary
func _init() -> void:
if not stats:
reset_stats()
func reset_stats() -> void:
stats = base_stats.duplicate()
Log.pr("StatsComponent reset to base stats")
func get_stat(stat_name: String) -> Variant:
var stat = get_nested_stat(stat_name)
if stat:
return stat
else:
Log.pr("Stat not found: ", stat_name)
return null
func update_stat(stat_name: String, value: Variant) -> void:
var updating_stat = get_nested_stat(stat_name)
if updating_stat:
set_nested_stat(stat_name, value)
Log.pr("Updated stat: ", stat_name, " to ", value)
else:
Log.pr("Stat not found: ", stat_name)
func get_nested_stat(path: String) -> Variant:
var keys = path.split(".")
var current = stats
for key in keys:
if current is Dictionary and current.has(key):
current = current[key]
else:
return null # Path doesn't exist
return current
func set_nested_stat(path: String, value) -> bool:
var keys = path.split(".")
var current = stats
# Navigate to the parent of the final key
for i in range(keys.size() - 1):
var key = keys[i]
# Check if key exists and is a dictionary
if not current.has(key) or not current[key] is Dictionary:
Log.error("Invalid stat path: " + path + " (key '" + key + "' doesn't exist or isn't a dictionary)")
return false
current = current[key]
# Check if final key exists
var final_key = keys[keys.size() - 1]
if not current.has(final_key):
Log.error("Invalid stat path: " + path + " (key '" + final_key + "' doesn't exist)")
return false
# Set the value at the final key
current[final_key] = value
return true

View file

@ -1,13 +0,0 @@
class_name FireRateAdditive extends Modifier
@export var fire_rate_bonus: float = 1.0 # +1 shot per second
func _init():
id = "fire_rate_additive"
display_name = "Rapid Fire"
description = "Increases fire rate by %0.1f shots per second" % fire_rate_bonus
modifier_type = ModifierType.ADDITIVE
func apply_stats_modification(final_stats: Dictionary, _base_stats: Dictionary) -> void:
if final_stats.has("fire_rate"):
final_stats.fire_rate += fire_rate_bonus

View file

@ -1,25 +0,0 @@
class_name ProjectileSizeAdditive extends Modifier
@export var size_increase: float = 0.5 # +50% bigger
func _init():
id = "size_additive"
display_name = "Enlarged Projectiles"
description = "Increases projectile size by %d%%" % (size_increase * 100)
modifier_type = ModifierType.ADDITIVE
func apply_stats_modification(final_stats: Dictionary, base_stats: Dictionary) -> void:
if final_stats.has("projectile_size"):
final_stats.projectile_size += size_increase
func modify_projectile(projectile) -> void:
# This will be called when the projectile is created
# Scale is often handled in the recalculate_stats method, but we can also add visual effects here
projectile.connect("on_spawned", _on_projectile_spawned)
func _on_projectile_spawned(projectile):
# Add a trail effect for larger projectiles
if projectile.scale.x > 1.2:
pass
#var trail = preload("res://scenes/projectile_trail.tscn").instantiate()
#projectile.add_child(trail)

View file

@ -1,4 +1,4 @@
class_name ModifierManager extends Node class_name ModifierManagerOLD extends Node
signal modifier_added(modifier) signal modifier_added(modifier)
signal modifier_removed(modifier) signal modifier_removed(modifier)

View file

@ -5,7 +5,6 @@ extends CharacterBody2D
@export var ranged: RangedWeaponComponent @export var ranged: RangedWeaponComponent
@export var melee: MeleeWeaponComponent @export var melee: MeleeWeaponComponent
var weapon: RangedWeapon
var movement: PlayerMovement var movement: PlayerMovement
var combat: PlayerCombat var combat: PlayerCombat
@ -15,7 +14,6 @@ var last_direction = Vector2.DOWN
@onready var animated_sprite = $PlayerSprite @onready var animated_sprite = $PlayerSprite
func _ready(): func _ready():
weapon = $RangedWeapon
combat = PlayerCombat.new() combat = PlayerCombat.new()
movement = PlayerMovement.new() movement = PlayerMovement.new()
@ -27,14 +25,14 @@ func _ready():
combat.animated_sprite = animated_sprite combat.animated_sprite = animated_sprite
Log.pr("Adding projectile size additive modifier") Log.pr("Adding projectile size additive modifier")
#weapon.add_modifier(ProjectileSizeAdditive.new()) ranged.add_modifier(ProjectileSizeAdditive.new())
Log.pr(weapon.stats.get_stat("projectile_size")) # Size is now 1.0 + 0.5 = 1.5 #Log.pr(weapon.stats.get_stat("projectile_size")) # Size is now 1.0 + 0.5 = 1.5
# Size is now 1.0 + 0.5 = 1.5 # Size is now 1.0 + 0.5 = 1.5
# Add the multiplicative size modifier (1.5x multiplier) # Add the multiplicative size modifier (1.5x multiplier)
Log.pr("Adding projectile size multiplicative modifier") Log.pr("Adding projectile size multiplicative modifier")
#weapon.add_modifier(ProjectileSizeMultiplicative.new()) #weapon.add_modifier(ProjectileSizeMultiplicative.new())
Log.pr(weapon.stats.get_stat("projectile_size")) #Log.pr(weapon.stats.get_stat("projectile_size"))
# Size is now 1.5 * 1.5 = 2.25 # Size is now 1.5 * 1.5 = 2.25
# Add another additive size modifier (+0.7 or 70% increase) # Add another additive size modifier (+0.7 or 70% increase)
@ -42,9 +40,9 @@ func _ready():
var another_size_mod = ProjectileSizeAdditive.new() var another_size_mod = ProjectileSizeAdditive.new()
another_size_mod.size_increase = 0.7 another_size_mod.size_increase = 0.7
#weapon.add_modifier(another_size_mod) #weapon.add_modifier(another_size_mod)
Log.pr(weapon.stats.get_stat("projectile_size")) #Log.pr(weapon.stats.get_stat("projectile_size"))
#weapon.add_modifier(FireRateAdditive.new()) ranged.add_modifier(FireRateAdditive.new())
func _physics_process(delta): func _physics_process(delta):

View file

@ -11,7 +11,7 @@ func process(_delta):
var direction = mouse_position - player_position var direction = mouse_position - player_position
var normalized_direction = direction.normalized() var normalized_direction = direction.normalized()
player.weapon.fire(normalized_direction, mouse_position) player.ranged.fire(normalized_direction, mouse_position)
# Update animation # Update animation
#update_animation() #update_animation()

View file

@ -17,15 +17,14 @@ var base_stats = {
} }
# Components # Components
var stats: ModifierManager #var stats: ModifierManager
var can_fire: bool = true var can_fire: bool = true
var fire_timer: Timer var fire_timer: Timer
func _init() -> void: func _init() -> void:
stats = ModifierManager.new(base_stats) #stats = ModifierManager.new(base_stats)
Log.pr(stats) #Log.pr(stats)
add_child(stats) #add_child(stats)
# Setup fire timer # Setup fire timer
fire_timer = Timer.new() fire_timer = Timer.new()
add_child(fire_timer) add_child(fire_timer)
@ -34,7 +33,7 @@ func _init() -> void:
projectile_scene = preload("res://assets/projectiles/projectile_lightning.tscn") projectile_scene = preload("res://assets/projectiles/projectile_lightning.tscn")
func _ready(): func _ready():
stats.connect("stats_updated", _on_stats_updated) #stats.connect("stats_updated", _on_stats_updated)
fire_timer.connect("timeout", _on_fire_timer_timeout) fire_timer.connect("timeout", _on_fire_timer_timeout)
# Initial update # Initial update
@ -48,13 +47,13 @@ func fire(direction: Vector2, target_position: Vector2):
_spawn_projectile(global_position, direction, target_position) _spawn_projectile(global_position, direction, target_position)
can_fire = false can_fire = false
Log.pr("Cooldown", stats.get_stat("fire_rate")) #Log.pr("Cooldown", stats.get_stat("fire_rate"))
fire_timer.start(stats.get_stat("fire_rate")) #fire_timer.start(stats.get_stat("fire_rate"))
func _spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2, target_position: Vector2): func _spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2, target_position: Vector2):
# Get projectile quantity and spread from stats # Get projectile quantity and spread from stats
var quantity = stats.get_stat("projectile_quantity") var quantity = 1 # stats.get_stat("projectile_quantity")
var spread_angle = stats.get_stat("projectile_spread") var spread_angle = 0 # stats.get_stat("projectile_spread")
# Calculate the angle between each projectile # Calculate the angle between each projectile
var angle_step = 0.0 var angle_step = 0.0
@ -79,18 +78,18 @@ func _spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2, target
projectile.direction = direction projectile.direction = direction
# Apply stats to projectile # Apply stats to projectile
projectile.speed = stats.get_stat("projectile_speed") projectile.speed = 200 # stats.get_stat("projectile_speed")
projectile.damage = stats.get_stat("damage") projectile.damage = 10 # stats.get_stat("damage")
projectile.lifetime = stats.get_stat("projectile_lifetime") projectile.lifetime = 200 # stats.get_stat("projectile_lifetime")
projectile.source_weapon = self projectile.source_weapon = self
# Set base size # Set base size
var size = stats.get_stat("projectile_size") var size = 1 # stats.get_stat("projectile_size")
projectile.set_projectile_scale(Vector2(size, size)) projectile.set_projectile_scale(Vector2(size, size))
# Allow modifiers to directly modify the projectile # Allow modifiers to directly modify the projectile
for modifier in stats.modifiers: #for modifier in stats.modifiers:
modifier.modify_projectile(projectile) # modifier.modify_projectile(projectile)
# Add to scene tree # Add to scene tree
if get_tree() and get_tree().get_root(): if get_tree() and get_tree().get_root():
@ -102,10 +101,11 @@ func _spawn_projectile(spawn_position: Vector2, spawn_direction: Vector2, target
func add_modifier(modifier: Modifier): func add_modifier(modifier: Modifier):
Log.pr("Adding modifier: ", modifier) Log.pr("Adding modifier: ", modifier)
stats.add_modifier(modifier) #stats.add_modifier(modifier)
func remove_modifier(modifier_id: String): func remove_modifier(modifier_id: String):
stats.remove_modifier(modifier_id) pass
#stats.remove_modifier(modifier_id)
func _on_stats_updated(): func _on_stats_updated():
# Update any visual components based on new stats # Update any visual components based on new stats