In a list of essential features for strategy games, attacking would probably be sorted in first or second place. Of course something that important should not be missing from the openage API. This time we are going to look at how units damage each other with the
Other articles in the modding API series:
- Units, Buildings & more
- Attack (you're here)
- Inventory System
- Too many villagers!
- Restocking farms
In the current API design, damage is dealt as a flat amount and is always directed against a specific type of armor. A single
Attack can damage several armor types at once through the definition of
ArmorAttack objects in the
damage member. Every
ArmorAttack stores the damage done against a type of armor. On execution of the attack, each damage value from
ArmorAttack the objects of the attacker is matched against a defender's
ArmorDefense object with the same armor type.
It's best if we look at a simple example before explaining the system further.
The above diagram gives us very basic definitions of one unit's
Attack and another unit's
Defense abilities. For convenience, a table shows the comparisons between the corresponding damage and armor values of the armor types.
The calculation for the
Pierce armor types is very straight forward.
ArmorDefense is subtracted from
ArmorAttack which yields the following results:
Damage against Melee armor: 3 - 0 = 3
Damage against Pierce armor: 5 - 4 = 1
The situation for the
Crush armor type is a bit more tricky because subtracting
damage would result in a negative value. This behavior is generally undesirable as negative damage can have weird effects. Therefore, the engine will always round negative damage against an armor type up to
Damage against Crush armor: 1 - 5 = -4 --> rounded up to 0
For the last armor type we are facing another special yet common case of the damage calculation. The attacking unit has an
ArmorAttack object for the
Fire armor type, but the defender does not have an
ArmorDefense object with matching armor type. Intuitively one might assume that the amount of fire damage dealt is
2, since the defender seems to have no armor against it. However, this is not true. The engine will only calculate damage when there is an armor type match between an
ArmorAttack and an
ArmorDefense object. If there is no match, the damage defaults to
0. We've already seen the correct way to give a unit no defense against an armor type when we look at
MeleeDefense. Here any attack against
Meelee armor does full damage because the defender has the matching armor type and
armor_value is set to
The behavior is a bit unintuitive because it inevitably means that an attack will deal no damage when a unit has no
ArmorDefense objects defined in
armors, which goes against the expectations of the majority of people. On the other hand, resistances against specific types of damage can be modelled much easier.
Damage against Fire armor: 0
In the end the individual damage values are summed up and then compared to
min_damage of the
Attack ability. The engine chooses the greater of the two values. In this example
1 which is smaller than the calculated damage value
4. The overall damage is therefore
Overall damage: max(1 , 3 + 1 + 0 + 0) = max(1 , 4) = 4
At runtime there can be situations where the damage is modified further, e.g. because of a height advantage. But that's a story for another time. It is also important to note that openage will likely support other attack systems that use a different damage calculation, like percentage based armor or attacks with a block chance, in the future.
Other types of Attack
In addition to the normal
Attack ability, the API supports three more special versions of attacking.
AreaAttack does area of effect damage with an optional damage dropoff over distance. The
SelfDestruct ability is an even more special
AreaAttack where the unit kills itself during the attack. With
RangedAttack the attacking unit can attack from a specified distance and does not have to stand right next to the target.
RangedAttack must not be confused with
ProjectileAttack, since the former does not require any projectile related data.
The first striking difference between
ProjectileAttack is that they are not directly related (other than the previous "special" versions of
Attack). This is rooted in the fact that
ProjectileAttack merely enables a unit to fire one or more projectiles. Each projectile can have its own
Attack ability that is executed on hit. In other words: The units with
ProjectileAttack are not doing damage, it's the projectiles they shoot.
The data for attacking with projectiles is partioned between the
ProjectileAttack ability and the
Projectile objects. In the ability attack range, number of projectiles per attack and of course the projectiles themselves are defined. Inside the
Projectile objects we store the actual
Attack and other related properties such as accuracy, whether it is fired in an arc or not, whether it is allowed to pass through units. This is great because it allows for a lot of customization. A unit could fire 20 different projectiles, each with individual properties and
Although behavior for abilities like
Attack is hardcoded in an engine function, the execution outcome should be allowed to deviate slightly. Preferrably we would want a way to define behavior edge cases have an influence on the calculation, e.g. when we attack with a height advantage. How that is handled is discussed next week, when we take a look at
Any more questions? Let us know and discuss those ideas by visiting our subreddit /r/openage!
As always, if you want to reach us directly in the dev chatroom: