Implements modifier and stats system

Adds a modifier and stats system to manage combat-related attributes.

Introduces StatsComponent for storing entity statistics and ModifierManager for applying dynamic modifiers. Recalculates stats based on modifier type and priority.

Updates projectile and weapon components to utilize the new stats system.
This commit is contained in:
Dan Baker 2025-05-07 11:52:30 +01:00
parent f97521decc
commit 19cc8cb573
13 changed files with 178 additions and 65 deletions

View file

@ -2,5 +2,91 @@
extends Node2D extends Node2D
class_name ModifierManagerTwo class_name ModifierManagerTwo
func _init() -> void: signal modifier_added(modifier)
Log.pr("ModifierManagerTwo initialized") signal modifier_removed(modifier)
signal stats_updated()
var stats: StatsComponent
# Stores all active modifiers
var modifiers: Array[Modifier] = []
# Base stats (before modifiers)
var base_stats: Dictionary = {}
# Final calculated stats
var final_stats: Dictionary = {}
func _ready() -> void:
Log.pr("ModifierManager initialized")
Log.pr("Stats: ", stats)
func set_stats(stats_component: StatsComponent) -> void:
self.stats = stats_component
func add_modifier(modifier: Modifier) -> void:
modifiers.append(modifier)
modifier.on_equip(get_parent())
emit_signal("modifier_added", modifier)
recalculate_stats()
func remove_modifier(modifier_id: String) -> void:
for i in range(modifiers.size()):
if modifiers[i].id == modifier_id:
var modifier = modifiers[i]
modifier.on_unequip(get_parent())
modifiers.remove_at(i)
emit_signal("modifier_removed", modifier)
recalculate_stats()
break
func recalculate_stats() -> void:
# Reset stats to base values
final_stats = base_stats.duplicate()
# Sort modifiers by priority
modifiers.sort_custom(func(a, b): return a.priority > b.priority)
# First pass: Apply OVERRIDE modifiers (highest priority first)
for modifier in modifiers:
if modifier.modifier_type == Modifier.ModifierType.OVERRIDE:
_apply_modifier_stats(modifier)
# Second pass: Apply ADDITIVE modifiers
for modifier in modifiers:
if modifier.modifier_type == Modifier.ModifierType.ADDITIVE:
_apply_modifier_stats(modifier)
# Third pass: Apply MULTIPLICATIVE modifiers
for modifier in modifiers:
if modifier.modifier_type == Modifier.ModifierType.MULTIPLICATIVE:
_apply_modifier_stats(modifier)
# Last pass: Apply CONDITIONAL modifiers
for modifier in modifiers:
if modifier.modifier_type == Modifier.ModifierType.CONDITIONAL:
_apply_modifier_stats(modifier)
# Apply caps and floors to stats
_apply_stat_limits()
emit_signal("stats_updated")
func _apply_modifier_stats(modifier: Modifier) -> void:
if modifier.has_method("apply_stats_modification"):
modifier.apply_stats_modification(final_stats, base_stats)
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

@ -3,12 +3,26 @@ class_name ProjectileBaseTwo
var lifetime_timer: Timer var lifetime_timer: Timer
var direction: Vector2
var target_position: Vector2
var ELEMENTS = Global.ELEMENTS
var has_collided: bool = false
var stats = { var stats = {
"speed": 500.0, "speed": 500.0,
"damage": 10.0, "damage": 10.0,
"element": ELEMENTS.NONE,
"lifetime": 2, "lifetime": 2,
"pierce_count": 0
} }
func set_stat(stat_name: String, value: Variant):
stats[stat_name] = value
func get_stat(stat_name: String) -> Variant:
return stats.get(stat_name, null)
func destroy(): func destroy():
emit_signal("on_destroyed", self) emit_signal("on_destroyed", self)
queue_free() queue_free()

View file

@ -4,9 +4,3 @@
[node name="RockProjectile" type="Node2D"] [node name="RockProjectile" type="Node2D"]
script = ExtResource("1_8myby") script = ExtResource("1_8myby")
speed = null
damage = null
lifetime = null
direction = null
target_position = null
is_friendly = null

View file

@ -5,7 +5,7 @@ func _ready():
lifetime_timer = Timer.new() lifetime_timer = Timer.new()
add_child(lifetime_timer) add_child(lifetime_timer)
lifetime_timer.one_shot = true lifetime_timer.one_shot = true
lifetime_timer.wait_time = lifetime lifetime_timer.wait_time = stats.lifetime
lifetime_timer.connect("timeout", _on_lifetime_timeout) lifetime_timer.connect("timeout", _on_lifetime_timeout)
lifetime_timer.start() lifetime_timer.start()
@ -13,7 +13,19 @@ func _ready():
connect("body_entered", _on_body_entered) connect("body_entered", _on_body_entered)
func _physics_process(delta): func _physics_process(delta):
position += direction * speed * delta position += direction * stats.speed * delta
func _on_body_entered(body):
if body.is_in_group("enemies"):
body.take_damage(stats.damage)
## Check if the projectile is piercing
if not get_stat("pierce_count"):
# If not piercing, destroy the projectile
destroy()
else:
# If piercing, reduce the pierce count
set_stat("pierce_count", get_stat("pierce_count") - 1)
func _on_lifetime_timeout(): func _on_lifetime_timeout():
super._on_lifetime_timeout() super._on_lifetime_timeout()

View file

@ -1,18 +1,6 @@
@icon("res://assets/editor/64x64/fc728.png") @icon("res://assets/editor/64x64/fc728.png")
extends WeaponComponent extends Node2D
class_name MeleeWeaponComponent class_name MeleeWeaponComponent
var stats = {
"piercing": 3,
}
var combined_stats = {}
func _init() -> void: func _init() -> void:
Log.pr("MeleeWeaponComponent initialized") Log.pr("MeleeWeaponComponent initialized")
super._init()
# Combine the base stats with the stats from the parent class
combined_stats = base_stats.duplicate()
combined_stats.merge(stats)
Log.pr("Combined stats: ", combined_stats)

View file

@ -1,25 +1,11 @@
@icon("res://assets/editor/64x64/fc1515.png") @icon("res://assets/editor/64x64/fc1515.png")
extends WeaponComponent extends Node2D
class_name RangedWeaponComponent class_name RangedWeaponComponent
var stats = { @export var stats: StatsComponent
"projectile_speed": 500.0,
"projectile_size": 1.0,
"projectile_lifetime": 1.0,
"projectile_quantity": 1,
"projectile_spread": 33,
"max_pierce": 0
}
var combined_stats = {}
func _init() -> void: @onready var modifier_manager = $ModifierManager
func _ready() -> void:
Log.pr("RangedWeaponComponent initialized") Log.pr("RangedWeaponComponent initialized")
super._init() modifier_manager.set_stats(stats)
# Combine the base stats with the stats from the parent class
combined_stats = base_stats.duplicate()
combined_stats.merge(stats)
Log.pr("Combined stats: ", combined_stats)
Log.pr("ModifierManager: ", modifier_manager)

View file

@ -1,18 +0,0 @@
@icon("res://assets/editor/64x64/fc729.png")
extends Node2D
class_name WeaponComponent
var modifier_manager
var base_stats = {
"damage": 10.0,
"attack_rate": 3.0
}
func _init() -> void:
Log.pr("WeaponComponent initialized")
func _ready() -> void:
await get_tree().process_frame
modifier_manager = $ModifierManager
Log.pr("ModifierManager: ", modifier_manager)

View file

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

View file

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://bjybfg0xrowb5"]
[ext_resource type="Script" uid="uid://bd63bbuh27fj0" path="res://entities/scripts/stats_component.gd" id="1_ll8eh"]
[node name="StatsComponent" type="Node2D"]
script = ExtResource("1_ll8eh")

View file

@ -0,0 +1,32 @@
@icon("res://assets/editor/64x64/fc681.png")
extends Node2D
class_name StatsComponent
var stats: Dictionary[String, Variant] = {
"base": {
"health": 100,
"max_health": 100,
"armor": 0,
"max_armor": 0,
"energy": 100,
"max_energy": 100,
"speed": 200.0,
},
"melee": {
"damage": 10,
"attack_rate": 1,
"element": Global.ELEMENTS.NONE,
},
"ranged": {
"damage": 10,
"attack_rate": 1,
"element": Global.ELEMENTS.NONE,
"projectile_speed": 500.0,
"projectile_size": 1.0,
"projectile_lifetime": 1.0,
"projectile_quantity": 1,
"projectile_explode_quantity": 0,
"projectile_explode_damage": 0.5,
"pierce_count": 0
}
}

View file

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

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=85 format=3 uid="uid://bo5aw2cad3akl"] [gd_scene load_steps=86 format=3 uid="uid://bo5aw2cad3akl"]
[ext_resource type="Script" uid="uid://bq038uo4cm6nv" path="res://player/scripts/player.gd" id="1_oul6g"] [ext_resource type="Script" uid="uid://bq038uo4cm6nv" path="res://player/scripts/player.gd" id="1_oul6g"]
[ext_resource type="Texture2D" uid="uid://dqgq2c1h6yk3k" path="res://assets/sprites/characters/pink/Pink_Monster_Attack1_4.png" id="2_yllr7"] [ext_resource type="Texture2D" uid="uid://dqgq2c1h6yk3k" path="res://assets/sprites/characters/pink/Pink_Monster_Attack1_4.png" id="2_yllr7"]
@ -16,6 +16,7 @@
[ext_resource type="PackedScene" uid="uid://cgxn1f4p4vik6" path="res://assets/weapons/ranged_weapon.tscn" id="14_kb6p2"] [ext_resource type="PackedScene" uid="uid://cgxn1f4p4vik6" path="res://assets/weapons/ranged_weapon.tscn" id="14_kb6p2"]
[ext_resource type="PackedScene" uid="uid://dud7c465danl4" path="res://combat/weapons/RangedWeaponComponent.tscn" id="15_wodsf"] [ext_resource type="PackedScene" uid="uid://dud7c465danl4" path="res://combat/weapons/RangedWeaponComponent.tscn" id="15_wodsf"]
[ext_resource type="PackedScene" uid="uid://dqful6et42ok8" path="res://combat/weapons/MeleeWeaponComponent.tscn" id="16_32hag"] [ext_resource type="PackedScene" uid="uid://dqful6et42ok8" path="res://combat/weapons/MeleeWeaponComponent.tscn" id="16_32hag"]
[ext_resource type="PackedScene" uid="uid://bjybfg0xrowb5" path="res://entities/StatsComponent.tscn" id="17_tqiix"]
[sub_resource type="CircleShape2D" id="CircleShape2D_rkbax"] [sub_resource type="CircleShape2D" id="CircleShape2D_rkbax"]
@ -565,6 +566,9 @@ zoom = Vector2(2, 2)
[node name="RangedWeapon" parent="." instance=ExtResource("14_kb6p2")] [node name="RangedWeapon" parent="." instance=ExtResource("14_kb6p2")]
[node name="RangedWeaponComponent" parent="." instance=ExtResource("15_wodsf")] [node name="RangedWeaponComponent" parent="." node_paths=PackedStringArray("stats") instance=ExtResource("15_wodsf")]
stats = NodePath("../StatsComponent")
[node name="MeleeWeaponComponent" parent="." instance=ExtResource("16_32hag")] [node name="MeleeWeaponComponent" parent="." instance=ExtResource("16_32hag")]
[node name="StatsComponent" parent="." instance=ExtResource("17_tqiix")]

View file

@ -11,4 +11,13 @@ const MAP_EMPTY = 0
const MAP_PATH = 1 const MAP_PATH = 1
const MAP_START = 2 const MAP_START = 2
const MAP_FINISH = 4 const MAP_FINISH = 4
const MAP_UP_CELL = 3 const MAP_UP_CELL = 3
const ELEMENTS = {
"NONE": 0,
"FIRE": 1,
"WATER": 2,
"EARTH": 3,
"AIR": 4,
"THUNDER": 5,
}