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 Attack
ability.
Other articles in the modding API series:
- Introduction
- Units, Buildings & more
- Abilities
- Patching
- Attack (you're here)
- Bonus
- Inventory System
- Too many villagers!
- Transform
- Civilizations
- Restocking farms
Damage calculation
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.
armor_type | damage | armor_value |
---|---|---|
Melee | 3 | 0 |
Pierce | 5 | 4 |
Crush | 1 | 5 |
Fire | 2 | N/A |
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 Melee
and Pierce
armor types is very straight forward. armor_value
in ArmorDefense
is subtracted from damage
in ArmorAttack
which yields the following results:
1 |
|
1 |
|
The situation for the Crush
armor type is a bit more tricky because subtracting armor_value
from 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 0
.
1 |
|
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 0
.
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.
1 |
|
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 min_damage
was 1
which is smaller than the calculated damage value 4
. The overall damage is therefore 4
.
1 |
|
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.
Shooting projectiles
The first striking difference between Attack
and 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 Attack
values.
Questions?
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 Bonus
objects.
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:
- Matrix:
#sfttech:matrix.org
- IRC:
#sfttech
on libera.chat