Last active
May 9, 2025 18:56
-
-
Save partybusiness/6afbced830cbdd3637b4025e2252a968 to your computer and use it in GitHub Desktop.
Pattern for numerical stats that can have buffs or debuffs applied.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends Resource | |
class_name Stat | |
# keeps track a a numerical stat and connected buffs / debuffs | |
# name of stat, also used as a matching ID for buffs | |
@export var name:String = "" | |
# whether value needs to be recalculated | |
var dirty:bool = false | |
# value of the stat after all buffs are added to base_value | |
var value:float: | |
get: | |
if dirty: | |
recalculate() | |
return value | |
# starting value without buffs | |
@export var base_value:float = 0: | |
set(new_value): | |
base_value = new_value | |
dirty = true | |
# list of buffs attached to this stat | |
var buffs:Array[StatBuff] = [] | |
func _init(new_name:String, start_value:float = 0.0) -> void: | |
name = new_name | |
base_value = start_value | |
# recalculates value based on current list of buffs | |
func recalculate() -> void: | |
var temp_value = base_value | |
var unstacked:Dictionary = {} # list of buff changes that need special handling | |
for buff in buffs: | |
if buff.no_stacking: | |
# use the highest change instead of letting them add together | |
if unstacked.has(buff.source): | |
unstacked[buff.source] = max(buff.change, unstacked[buff.source]) | |
else: | |
unstacked[buff.source] = buff.change | |
else: # handle normally | |
temp_value += buff.change | |
for change in unstacked: | |
temp_value += change | |
value = temp_value | |
dirty = false | |
# adds a buff | |
func add_buff(new_buff:StatBuff) -> void: | |
# TODO should we prevent duplicate buffs? | |
buffs.append(new_buff) | |
dirty = true | |
# removes a given buff | |
# returns whether it had the buff to remove | |
func remove_buff(old_buff:StatBuff) -> bool: | |
if buffs.has(old_buff): | |
buffs.remove_at(buffs.find(old_buff)) | |
dirty = true | |
return true | |
else: | |
return false | |
# clears all buffs | |
func clear_buffs() -> void: | |
buffs.clear() | |
dirty = true | |
# remove any buffs that came from a named source | |
# int is how many were found | |
func remove_buff_source(source:String) -> int: | |
var remove_list:Array[StatBuff] = [] | |
for buff in buffs: | |
if buff.source == source: | |
remove_list.append(buff) | |
dirty = true | |
for buff in remove_list: | |
if buffs.has(buff): | |
buffs.remove_at(buffs.find(buff)) | |
return remove_list.size() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends Resource | |
class_name StatBuff | |
# adds a value to a given stat, can easily be debuff if change is negative | |
# name of the stat this will be applied to | |
@export var stat_name:String = "" | |
# name of how much it changes the underlying stat | |
@export var change:float = 0.0 | |
# name of the source of this buff | |
@export var source:String = "" | |
func _init(new_name:String, new_change:float, new_source:String = "") -> void: | |
stat_name = new_name | |
change = new_change | |
source = new_source |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extends Resource | |
class_name StatContainer | |
# contains any number of stats | |
@export var stats:Array[Stat] | |
var dirty:bool = false | |
var baked_stats:Dictionary = {} | |
# adds a stat | |
func add_stat(stat_name:String, stat_value:float = 0.0) -> void: | |
# if the stat alredy exists, just update base value | |
for stat in stats: | |
if stat.name == stat_name: | |
stat.base_value = stat_value | |
dirty = true | |
return | |
# otherwise, create the stat and add it to the list | |
var new_stat:Stat = Stat.new(stat_name, stat_value) | |
stats.append(new_stat) | |
# since this stat has no buffs yet, we can just directly set the baked value and not bother with dirty | |
if !baked_stats.has(stat_name): | |
baked_stats[stat_name] = stat_value | |
# removes a stat | |
func remove_stat(stat_name:String) -> void: | |
for stat in stats: | |
if stat.name == stat_name: | |
stats.remove_at(stats.find(stat)) | |
# remove it from the baked stats | |
if baked_stats.has(stat_name): | |
baked_stats.erase(stat_name) | |
# that is the only bake change required so we don't bother setting dirty | |
return | |
# applies a buff to the matching stat | |
# returns True or False based on whether a matching stat is found | |
func apply_buff(buff:StatBuff) -> bool: | |
if buff == null: | |
return false | |
# search stats for matching name | |
for stat in stats: | |
if stat.name == buff.stat_name: | |
stat.add_buff(buff) | |
dirty = true | |
return true | |
return false | |
# applies a list of buffs a returns count of how many were applied | |
func apply_buffs(buffs:Array[StatBuff]) -> int: | |
var count:int = 0 | |
for buff in buffs: | |
if apply_buff(buff): | |
count += 1 | |
return count | |
# applies a buff to the matching stat | |
# returns true or false based on whether a matching stat is found | |
func remove_buff(buff:StatBuff) -> bool: | |
if buff == null: | |
return false | |
# search stats for matching name | |
for stat in stats: | |
if stat.name == buff.stat_name: | |
if stat.remove_buff(buff): | |
dirty = true | |
return true | |
else: | |
return false | |
return false | |
# removes a list of buffs | |
func remove_buffs(buffs:Array[StatBuff]) -> void: | |
for buff in buffs: | |
remove_buff(buff) | |
# removes all buffs from a named source | |
func remove_buff_source(source:String) -> void: | |
for stat in stats: | |
if stat.remove_buff_source(source) > 0: | |
dirty = true | |
# fetches the value of a given stat by name | |
func get_value(stat_name:String) -> float: | |
if dirty: | |
rebake_stats() | |
if baked_stats.has(stat_name): | |
return baked_stats[stat_name] | |
return 0.0 | |
# bakes stats into a dictionary for easy reference by name | |
func rebake_stats() -> void: | |
dirty = false | |
baked_stats.clear() | |
for stat in stats: | |
baked_stats[stat.name] = stat.value |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment