Skip to content

Instantly share code, notes, and snippets.

@partybusiness
Last active May 9, 2025 18:56
Show Gist options
  • Save partybusiness/6afbced830cbdd3637b4025e2252a968 to your computer and use it in GitHub Desktop.
Save partybusiness/6afbced830cbdd3637b4025e2252a968 to your computer and use it in GitHub Desktop.
Pattern for numerical stats that can have buffs or debuffs applied.
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()
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
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