With the design of our formulas in mind, it is now time to implement them. To do so, we are going to create a library of static functions1 in a class named Formulas
. But first, we should define the available elements of our battle system because other calculations depend on it.
To code the elements, we’re going to create a class that only holds global constants. I’ve named it Types.gd
:
# Using `class_name` allows us to access the constants from any other file. class_name Types extends Reference # This is the same enum we wrote in the ActionData classes. enum Elements { NONE, CODE, DESIGN, ART, BUG } # Mapping between an element and the element against which it's strong. const WEAKNESS_MAPPING = { # A value of -1 makes the element strong or weak against nothing. Elements.NONE: -1, # For example, the line below means that code is strong against art. Elements.CODE: Elements.ART, Elements.ART: Elements.DESIGN, Elements.DESIGN: Elements.CODE, Elements.BUG: -1, }
We can access the properties from anywhere. Let’s see how by completing BattlerStats.gd
. Open the file and add the following properties:
# An array of elements against which the battler is weak. # These weaknesses should be values from our `Types.Elements` enum. export var weaknesses := [] # The battler's elemental affinity. Gives bonuses with related actions. export (Types.Elements) var affinity: int = Types.Elements.NONE
A limitation of GDScript is that enums are lists of integers, and so you cannot use them as a type hint: the affinity
variable will be an integer and you can assign any value to it, event one that’s not in Types.Elements
.
However, we can use Types.Elements
as an export hint, in parentheses above. This results in a drop-down menu when setting the variable in the Inspector.
Let’s implement the formulas now. Create a new file named Formulas.gd
.
We are going to use static functions so we can safely access them from any GDScript file. The most important functions below are calculate_base_damage()
and calculate_hit_chance()
. They correspond to the two formulas we discussed in the previous lesson.
class_name Formulas extends Reference # Returns the product of the attacker's attack and the action's multiplier. static func calculate_potential_damage(action_data, attacker) -> float: return attacker.stats.attack * action_data.damage_multiplier # The base damage is "attacker.attack * action.multiplier - defender.defense". # The function multiplies it by a weakness multiplier, calculated by # `_calculate_weakness_multiplier` below. Finally, we ensure the value is an # integer in the [1, 999] range. static func calculate_base_damage(action_data, attacker, defender) -> int: var damage: float = calculate_potential_damage(action_data, attacker) damage -= defender.stats.defense damage *= _calculate_weakness_multiplier(action_data, defender) return int(clamp(damage, 1.0, 999.0)) # Calculates a multiplier based on the action and the defender's elements. static func _calculate_weakness_multiplier(action_data, defender) -> float: var multiplier := 1.0 var element: int = action_data.element if element != Types.Elements.NONE: # If the defender has an affinity with the action's element, the # multiplier should be 0.75 if Types.WEAKNESS_MAPPING[defender.stats.affinity] == element: multiplier = 0.75 # If the action's element is part of the defender's weaknesses, we # set the multiplier to 1.5 elif Types.WEAKNESS_MAPPING[element] in defender.stats.weaknesses: multiplier = 1.5 return multiplier
Here’s the function that calculates a hit’s chance. It returns a value between 0
and 100
. If the hit chance is 100
, the attack is guaranteed to hit. A value of 60
means the attack has a 60% chance to hit.
# The formula in pseudo-code: # (attacker.hit_chance - defender.evasion) * action.hit_chance + affinity_bonus + element_triad_bonus - defender_affinity_bonus static func calculate_hit_chance(action_data, attacker, defender) -> float: var chance: float = attacker.stats.hit_chance - defender.stats.evasion # The action's hit chance is a value between 0 and 100 for consistency with # the battlers' stats. As we use it as a multiplier here, we need to divide # it by 100 first. chance *= action_data.hit_chance / 100.0 # Below, we use the new BattlerStats properties, `affinity` and `weaknesses`, # to apply a hit chance bonus or penalty. var element: int = action_data.element # If the action's element matches the attacker's affinity, we increase the # hit rating by 5. if element == attacker.stats.affinity: chance += 5.0 if element != Types.Elements.NONE: # If the action's element is part of the defender's weaknesses, we # increase the hit rating by 10. if Types.WEAKNESS_MAPPING[element] in defender.stats.weaknesses: chance += 10.0 # However, if the defender has an affinity with the action's element, we # decrease the hit rating by 10. if Types.WEAKNESS_MAPPING[defender.stats.affinity] == element: chance -= 10.0 # Clamping the result ensures it's always in the [0, 100] range. return clamp(chance, 0.0, 100.0)
You may have noticed that the functions’ arguments lack type hints. This is because of cyclic reference errors in Godot 3.2, a problem that Godot 4.0 should hopefully address.
static func calculate_base_damage(action_data, attacker, defender) -> int:
In the function above, action_data
is of type ActionData
, and both attacker
and defender
should be of type Battler
.
Here is the complete code for reference.
Types.gd
:
class_name Types extends Reference enum Elements { NONE, CODE, DESIGN, ART, BUG } const WEAKNESS_MAPPING = { Elements.NONE: -1, Elements.CODE: Elements.ART, Elements.ART: Elements.DESIGN, Elements.DESIGN: Elements.CODE, Elements.BUG: -1, }
Formulas.gd
:
class_name Formulas extends Reference static func calculate_potential_damage(action_data, attacker) -> float: return attacker.stats.attack * action_data.damage_multiplier static func calculate_base_damage(action_data, attacker, defender) -> int: var damage: float = calculate_potential_damage(action_data, attacker) damage -= defender.stats.defense damage *= _calculate_weakness_multiplier(action_data, defender) return int(clamp(damage, 1.0, 999.0)) static func calculate_hit_chance(action_data, attacker, defender) -> float: var chance: float = attacker.stats.hit_chance - defender.stats.evasion chance *= action_data.hit_chance / 100.0 var element: int = action_data.element if element == attacker.stats.affinity: chance += 5.0 if element != Types.Elements.NONE: if Types.WEAKNESS_MAPPING[element] in defender.stats.weaknesses: chance += 10.0 if Types.WEAKNESS_MAPPING[defender.stats.affinity] == element: chance -= 10.0 return clamp(chance, 0.0, 100.0) static func _calculate_weakness_multiplier(action_data, defender) -> float: var multiplier := 1.0 var element: int = action_data.element if element != Types.Elements.NONE: if Types.WEAKNESS_MAPPING[defender.stats.affinity] == element: multiplier = 0.75 elif Types.WEAKNESS_MAPPING[element] in defender.stats.weaknesses: multiplier = 1.5 return multiplier
Static functions are functions that cannot access the class’s properties. They can only take arguments and return a value. This constraint ensures that you cannot mutate properties of the class. We also say that these functions do not have side-effects: calling them doesn’t cause changes in any system. In general, static functions are useful to create libraries of code you can share and reuse safely, unlike singletons.↩︎