I am not sure if you are reading this still but I have struggled with this sort of problem for a long time.
I've designed numerous different types of affect systems. I'll briefly go over them now. This is all based on my experience. I don't claim to know all of the answers.
Static Modifiers
This type of system mostly relies on simple integers to determine any modifications. For example, +100 to Max HP, +10 to attack and so on. This system could also handle percents as well. You just need to make sure that the stacking doesn't get out of control.
I never really cached the generated values for this type of system. For example, if I wanted to display the max health of something, I would generate the value on the spot. This prevented things from being error prone and just easier to understand for everyone involved.
(I work in Java so the what follows is Java based but it should work with some modifications for other languages) This system can easily be done using enums for the modification types, and then integers. The end result can be placed into some sort of collection that has key,value ordered pairs. This will be fast lookup and calculations, so the performance is very good.
Overall, it works very well with just flat out static modifiers. Though, code must exist in the proper places for the modifiers to be used: getAttack, getMaxHP, getMeleeDamage, and so on and so forth.
Where this method fails (for me) is very complex interaction between buffs. There isn't a real easy way to have interaction except by ghettoing it a up a bit. It does have some simple interaction possibilities. In order to do that, you must make a modification to the way you store the static modifiers. Instead of using an enum as the key, you use a String. This String would be the Enum name + extra variable. 9 times out of 10, the extra variable is not used, so you still retain the enum name as the key.
Let's do a quick example:
If you wanted to be able to modify damage against undead creatures, you could have an ordered pair like this:
(DAMAGE_Undead, 10)
The DAMAGE is the Enum and the Undead is the extra variable. So during your combat, you can do something like:
dam += attacker.getMod(Mod.DAMAGE + npc.getRaceFamily()); //in this case the race family would be undead
Anyways, it works fairly well and is fast. But it fails at complex interactions and having “special” code everywhere. For example, consider the situation of “25% chance to teleport on death”. This is a “fairly” complex one. The above system can handle it, but not easily, as you need the following:
- Determine if the player has this mod.
- Somewhere, have some code to execute the teleportation, if successful. The location of this code is a discussion in itself!
- Get the right data from the Mod map. What does the value mean? Is it the room where they teleport too? What if a player has two teleport mods on them?? Won't the amounts get added together?????? FAILURE!
So this brings me to my next one:
The Ultimate Complex Buff System
I once tried to write a 2D MMORPG by myself. This was an awful mistake but I learned a lot!
I rewrote the affect system 3 times. The first one used a less powerful variation of the above. The second one was what I'm going to talk about.
This system had a series of classes for each modification, so things like: ChangeHP, ChangeMaxHP, ChangeHPByPercent, ChangeMaxByPercent. I had a million of these guys – even things like TeleportOnDeath.
My classes had things that would do the following:
- applyAffect
- removeAffect
- checkForInteraction <--- important
Apply and remove explain themselves (though for things like percents, the affect would keep track of how much it increased the HP by to ensure when the affect wore off, it'd only remove the amount it added. This was buggy, lol, and took me a long time to make sure it was right. I still didn't get a good feeling about it.).
The checkForInteraction method was a horrodeously complex piece of code. In each of the affects (ie: ChangeHP) classes, it would have code to determine if this should be modified by the input affect. So for example, if you had something like....
- Buff 1: Deals 10 Fire damage on attack
- Buff 2: Increases all fire damage by 25%.
- Buff 3: Increases all fire damage by 15.
The checkForInteraction method would handle all of these affects. In order to do this, each affect on ALL of the players near by had to be checked!! This is because the type of affects I had dealt with multiple players over a span of an area. This means the code NEVER HAD any special statements like above - “if we just died, we should check for teleport on death”. This system would automatically handle it correctly at the right time.
Trying to write this system took me like 2 months and made by head explode several times. HOWEVER, it was REALLY powerful and could do an insane amount of stuff – especially when you take into account the following two facts for abilities in my game:
1. They had target ranges (ie: single, self, group only, PB AE self, PB AE target, targetted AE, and so on).
2. Abilities could have more than 1 affect on them.
As I mentioned above, this was the 2nd of 3rd affect system for this game. Why did I move away from this?
This system had the worst performance I've ever seen! It was awfully slow as it had to do so much checking for each thing that went on. I tried to improve it, but deemed it a failure.
So we come to my third version (and another type of buff system):
Complex Affect Class with Handlers
So this is pretty much a combination of the first two:
We can have static variables in an Affect class that contains a lot of functionality and extra data. Then just call handlers (for me, pretty much some static utility methods instead of subclasses for specific actions. But I'm sure you could go with subclasses for actions if you wanted too) when we want to do something.
The Affect class would have all of the juicy good stuffs, like target types, duration, number of uses, chance to execute and so on and so forth.
We would still have to add special codes in order to handle the situations, for example, teleport on death. We would still have to check for this in the combat code manually, and then if it existed, we would get a list of affects. This list of affects contains all of the currently applied affects on the player that dealt with teleporting on death. Then we would just look at each one and check to see if it executed and was successful (We'd stop at the first successful one). It it was successful, we'd just call the handler to take care of this.
Interaction can be done, if you want too. It'd just have to write the code to look for specific buffs on the players/etc. Because it has good performance (see below), it should fairly be efficient to do that. It just would need more complex handlers and so on.
So it has a lot of the performance of the first system and still a lot of complexity like the second (but not AS much). In Java at least, you can do some tricky things to get the performance of almost the first one in MOST cases (ie: having an enum map (http://docs.oracle.com/javase/6/docs/api/java/util/EnumMap.html) with Enums as the keys and ArrayList of affects as the values. This lets you to see if you quickly have affects [since the list would be 0 or the map wouldn't have the enum] and not having to continually iterate over player's affect lists for no reason. I don't mind iterating over affects if we need them at this time. I'll optimize later if it becomes a problem).
I'm currently re-opening (rewriting the game in Java instead of the FastROM code base it was originally in) my MUD that ended in 2005 and I have recently ran into how do I want to implement my buff system? I am going to be using this system because it worked nicely in my previous failed game.
Well, hopefully someone, somewhere, will find a few of these insights useful.