This is a complicated issue, because you're talking about a few different things that (these days) get lumped together as 'buffs':
- modifiers to a player's attributes
- special effects that happen on certain events
- combinations of the above.
I always implement the first with a list of active effects for a certain character. Removal from the list, whether based on duration or explicitly is fairly trivial so I won't cover that here. Each Effect contains a list of attribute modifiers, and can apply it to the underlying value via simple multiplication.
Then I wrap it with functions to access the modified attributes. eg.:
def get_current_attribute_value(attribute_id, criteria):
val = character.raw_attribute_value[attribute_id]
# Accumulate the modifiers
for effect in character.all_effects:
val = effect.apply_attribute_modifier(attribute_id, val, criteria)
# Make sure it doesn't exceed game design boundaries
val = apply_capping_to_final_value(val)
return val
class Effect():
def apply_attribute_modifier(attribute_id, val, criteria):
if attribute_id in self.modifier_list:
modifier = self.modifier_list[attribute_id]
# Does the modifier apply at this time?
if modifier.criteria == criteria:
# Apply multiplicative modifier
return val * modifier.amount
else:
return val
class Modifier():
amount = 1.0 # default that has no effect
criteria = None # applies all of the time
That lets you apply multiplicative effects easily enough. If you need additive effects also, decide what order you're going to apply them in (probably additive last) and run through the list twice. (I'd probably have separate modifier lists in Effect, one for multiplicative, one for additive).
The criteria value is to let you implement "+20% vs Undead" - set the UNDEAD value on the Effect and only pass the UNDEAD value to get_current_attribute_value()
when you're calculating a damage roll against an undead foe.
Incidentally, I wouldn't be tempted to try and write a system that applies and unapplies values directly to the underlying attribute value - the end result is that your attributes are very likely to drift away from the intended value due to error. (eg. if you multiply something by 2, but then cap it, when you divide it by 2 again, it'll be lower than it started with.)
As for event-based effects, such as "Deal 15 damage back to attackers when hit", you can add methods on the Effect class for that. But if you want distinct and arbitrary behaviour (eg. some effects for the above event might reflect damage back, some might heal you, it might teleport you away randomly, whatever) you'll need custom functions or classes to handle it. You can assign functions to event handlers on the effect, then you can just call the event handlers on any active effects.
# This is a method on a Character, called during combat
def on_receive_damage(damage_info):
for effect in character.all_effects:
effect.on_receive_damage(character, damage_info)
class Effect():
self.on_receive_damage_handler = DoNothing # a default function that does nothing
def on_receive_damage(character, damage_info):
self.on_receive_damage_handler(character, damage_info)
def reflect_damage(character, damage_info):
damage_info.attacker.receive_damage(15)
reflect_damage_effect = new Effect()
reflect_damage_effect.on_receive_damage_handler = reflect_damage
my_character.all_effects.add(reflect_damage_effect)
Obviously your Effect class will have an event handler for every type of event, and you can assign handler functions to as many as you need in each case. You don't need to subclass Effect, as each one is defined by the composition of the attribute modifiers and event handlers it contains. (It will probably also contain a name, a duration, etc.)