This is a concept of an object oriented, event driven combat system that, once finished, should be applicable to most/all private servers but it's just a framework for now. It's far from finished and there's still some things I need to work out, but so far it looks promising.
Pros to an event driven combat system:
- Less CPU spent to cycle through all unnecessary bulk code related to combat
- Better organization
- Specific behaviour can be dealt with more approperiately because of abstraction and OO techniques
Cons to an event driven combat system:
- Potentially uses more memory
- Has some overhead/boilerplate code
- Relies on good use of an event manager
On with the explanation:
At the core of the system is the CombatListener class. This class allows for events to be triggered during specific processes of the combat system. It is because of this class that this system does not have code laying around that shouldn't be there in the first place. Also, because of its design, it lets you modularly generate which actions should occur in which scenarios. Previously, when dealing with actions that should be triggered when a player dies, you would have to cram all the logic inside a single method that deals with the player's death. Should the items be dropped based on the player's context? Who is considered the killer of said player? Do we give points to another player for killing said player? All of this logic is only cycled through if that logic is dealt with in a CombatListener that is attributed to the players involved and therefore these logic blocks are only ever processed if they are relevant to the current context.
The biggest plus of this design should become apparent when doing combat calculations. Before, you would have to check each and every single possible scenario when a player attacks. Is he using prayer bonusses? Is he wearing a full barrows set? Is he using a berserker necklace in combination with an obsidian weapon? Instead of having a block of code to check for these statements (that is cycled through every time the player attacks, despite being completely irrelevant to the context incase a player is not benefiting from any of the aforementioned bonusses), the system only checks to see if bonusses should be applied incase they are relevant (A check is still required; a magic boosting prayer will only have its logic block checked if it's active, but that doesn't mean that it will trigger if the player is using melee).
Code:
package rs2.combat;
import rs2.combat.hit.Hit;
public class CombatListener {
public void buffAsAttacker(Hit hit) {
}
public void buffAsDefender(Hit hit) {
}
public void modifyAsAttacker(Hit hit) {
}
public void modifyAsDefender(Hit hit) {
}
public void onHitAsAttacker(Hit hit) {
}
public void onHitAsDefender(Hit hit) {
}
public void onDeath(Hit hit) {
}
}
Here's an example of how the CombatListener would look for the Protect From Melee prayer:
Code:
CombatListener ProtectFromMelee = new CombatListener() {
@Override
public void modifyAsDefender(Hit hit) {
if (hit.getType() == HitType.CRUSH || hit.getType() == HitType.SLASH || hit.getType() == HitType.STAB) {
if (hit.getAttacker().isPlayer()) {
hit.setDamage((int)(hit.getDamage()*0.6));
} else {
hit.setDamage(0);
}
}
}
};
Here's another example for the Vengeance spell.
Code:
CombatListener vengeance = new CombatListener() {
@Override
public void onHitAsDefender(Hit hit) {
if (hit.getType() != HitType.POISON && hit.getType() != HitType.TRUE_DAMAGE) {
if (hit.getAttacker() != null) {
TrueHit veng = new TrueHit(hit.getAttacker(), (int)(hit.getDamage()*0.75));
Player def = (Player)hit.getDefender();
def.sendMessage("Taste Vengeance!");
hit.getDefender().removeCombatListener("vengeance");
veng.process();
}
}
}
};
At this point I should probably also explain when these fields get called. The main protocol of hit-generation is as follows:
- Determine attacker and defender (not shown)
- Determine the type of hit this is going to be
- Determine the accuracy of attacker and his max hit
- Determine the defensive stats of the defender
- Buff the attacker's accuracy/max hit by cycling through his listeners -> buffAsAttacker()
- Buff the defender's ratings by cycling through his listeners -> buffAsDefender()
- Use the accuracy values to determine whether or not this hit will be successful
- Incase the hit goes through, determine the amount of damage to deal, based on the max hit
- Allow the attacker to "modify" the hit (after calculations) -> modifyAsAttacker()
- Allow the defender to "modify" the hit -> modifyAsDefender()
- Apply the hit once its delay has run out
- Apply any special effects associated with this hit
- Deal with any events triggered after dealing damage -> onHitAsAttacker()
- Deal with any event triggered after receiving damage -> onHitAsDefender()
(if you have any questions about this protocol, ask away)
As you might be able to tell, the Hit class also plays a significant role in this design. The Hit class encapsulates several core details of a hit; who is attacking, who is defending, how much damage the system eventually decided to hit and the type of the hit. For now, these are the possible hit types:
Code:
package rs2.combat.hit;
public enum HitType {
STAB,
SLASH,
CRUSH,
MAGIC,
RANGED,
POISON,
TRUE_DAMAGE,
DRAGONFIRE
}
One important thing to note is that one weapon can have different Hit-subtypes associated with it. A scimitar, for example, has 4 different attack styles, each of which can have their own Hit class associated with them (this allows us to increase defensive stats when using the defensive stance, increase strength when using the agressive stance, etc). Special Attacks are also entirely different Hit implementations because of the extra behaviour that they usually bring with them.
While there's a lot of different mechanics involved with all the different types and subtypes of hits that exist in Runescape, the core functionality remains the same. A hit is dealt to a defender by an attacker (or none incase of "system damage" or "true damage"). The Hit class serves the very purpose of embodying this core functionality:
Code:
package rs2.combat.hit;
import rs2.combat.Attackable;
public abstract class Hit {
private HitType type;
private Attackable attacker;
private Attackable defender;
private int damage;
public int getDamage() {
return damage;
}
public void setDamage(int dmg) {
this.damage = dmg;
}
public Attackable getAttacker() {
return attacker;
}
public Attackable getDefender() {
return defender;
}
public void setAttacker(Attackable attacker) {
this.attacker = attacker;
}
public void setDefender(Attackable defender) {
this.defender = defender;
}
public HitType getType() {
return type;
}
public void setType(HitType type) {
this.type = type;
}
public void process() {
defender.dealDamage(this);
applySpecialEffects();
}
public boolean isValid() {
return true;
}
public void applySpecialEffects() {
}
}
You may notice the fields isValid(), applySpecialEffects() and process(). These fields are there to provide access to more complex mechanics at a lower level.
isValid(), for example, is there to determine whether or not player can actually use their weapon (can't fire all ammo with all ranged weapons, does the player have the required runes for a spell, etc). This field has nothing to do with matters such as wilderness levels or distance between the target and us because these are matters that are have been validated before we can apply a hit.
applySpecialEffects() deals with events that are specific to a spell or a special attack. For example, the extra magic damage dealt by the Saradomin Sword's special attack would be dealt with here. The Dragon Scimitar special attack is another example. Special attack effects that give you increased accuracy and damage are dealt with in the hit-generation process in the that hit's class. The AoE damage and freeze effects of ice spells for example, are also dealt with in this method.
The final field is the process() field. When a hit is generated, it isn't necessarily triggered. Some attacks (magic and ranged particularly) have a delay before they hit the target. When a hit is registered, it is passed on to the event system which, once ready, triggers the hit to occur. This is the main use of the process(). Attacks that have no delay should still be passed on to the event system for consistency.
Let's look at an implementation of this class. Here's how a dragonfire hit would look like:
Code:
package rs2.combat.hit.impl;
import rs2.combat.Attackable;
import rs2.combat.CombatListener;
import rs2.combat.CombatUtility;
import rs2.combat.hit.Hit;
import rs2.combat.hit.HitType;
public class DragonfireHit extends Hit {
public static final int ORIGINAL_MAX_HIT = 60;
public static final int REDUCED_MAX_HIT = 3;
private boolean usingShield;
private boolean potionEffect;
private boolean usingPrayer;
public DragonfireHit(Attackable defender) {
setDefender(defender);
setType(HitType.DRAGONFIRE);
int max = ORIGINAL_MAX_HIT;
CombatListener[] cbls = defender.getListeners();
for (CombatListener cbl : cbls) {
cbl.buffAsDefender(this);
}
if (usingPrayer) {
max = REDUCED_MAX_HIT;
}
if (usingShield) {
max = REDUCED_MAX_HIT;
if (potionEffect) {
max = 0;
}
}
setDamage(CombatUtility.getRandomHit(max));
}
public void setUsingPrayer(boolean prayer) {
this.usingPrayer = prayer;
}
public void setUsingShield(boolean shield) {
this.usingShield = shield;
}
public void setPotionEffect(boolean potion) {
this.potionEffect = potion;
}
}
If it looks weird, I took all the info from the Oldschool Runescape Wiki. One behaviour that you will note on their article that I did not implement here is that the protect from magic prayer only reduces the damage from chromatic dragons (and not metallic ones), whereas my design makes no such adjustment. The King Black Dragon's specific dragon breath attacks would each be a subset of this class, while having its applySpecialEffects() fields overwritten to deal with their corresponding effects.
This class relies on a couple of things to work. It assumes that the Protect from Magic prayer is handled as a CombatListener, same goes for the effects of the Antifire Potion and having an (anti)dragonfire shield equipped. All of this makes sense, because this is what the system was designed for.
I'm still working on this design because there are some very very important details that I want to get out of the way before expanding it. I hope you all enjoyed this read and maybe one day I'll get to implement this into a functioning server.
Again, if you have any questions, shoot 'em.