With the damage formulas and Action
abstract base class, we can now code the attacks. Here’s where we put the command pattern into practice. Our AttackAction
objects are going to take control of the battler and targets, play animations, and more.
We need a new file that extends Action
and overrides Action._apply_async()
for attacks. An attack is going to hold one or more hits and apply them to target battlers.
Create a file named AttackAction.gd
with the following code.
# Concrete class for damaging attacks. Inflicts zero or more direct damage to # one or more targets. It can also apply status effects. class_name AttackAction extends Action # We calculate and store hits in an array to consume later, in sync with the # animation. var _hits := [] # We must override the constructor to use it. # Notice how _init() uses a unique notation to call the parent's constructor. func _init(data: AttackActionData, actor, targets: Array).(data, actor, targets) -> void: pass
Notice how in the constructor function above, _init()
, we pass a data
instance of type AttackActionData
. That’s because we need a few more properties on the resource associated with attacks. For that, create a new file, AttackActionData.gd
and make it extend ActionData
.
# Data container used to construct [AttackAction] objects. class_name AttackActionData extends ActionData # Multiplier applied to the calculated attack damage. export var damage_multiplier := 1.0 # Hit chance rating for this attack. Works as a rate: a value of 90 means the # action has a 90% chance to hit. export var hit_chance := 100.0
To attack a target, we’re going to use a typical object-oriented approach that consists of two steps:
Let’s create a new Hit.gd
script.
# Represents a damage-dealing hit to be applied to a target Battler. # Encapsulates calculations for how hits are applied based on some properties. class_name Hit extends Reference # The damage dealt by the hit. var damage := 0 # Chance to hit in base 100. var hit_chance: float func _init(_damage: int, _hit_chance := 100.0) -> void: damage = _damage hit_chance = _hit_chance # Returns true if the hit isn't missing. To use when consuming the hit. func does_hit() -> bool: return randf() * 100.0 < hit_chance
There are two main reasons to bother using a class like Hit
:
In short, it can make it easier to change the code later.
Going back to AttackAction.gd
, we can implement the attack logic. The code below is temporary. We’re going to lay down the foundations for now and later revisit the class to code hits that synchronize with animations.
# Returns the damage dealt by this action. We will update this function # when we implement status effects. func calculate_potential_damage_for(target) -> int: return Formulas.calculate_base_damage(_data, _actor, target) func _apply_async() -> bool: # We apply the action to each target so attacks work both for single and multiple targets. for target in _targets: var hit_chance := Formulas.calculate_hit_chance(_data, _actor, target) var damage := calculate_potential_damage_for(target) var hit := Hit.new(damage, hit_chance) # We're going to define a new function on the battler so it takes hits. target.take_hit(hit) # Our method is supposed to be a coroutine, that is to say, it pauses # execution and ends after some time. # in Godot 3.2, we do so using the `yield` keyword. # Here, we wait for the next frame by listening to the SceneTree's # `idle_frame` signal. yield(Engine.get_main_loop(), "idle_frame") return true
And that’s a new feature in. Our battlers still can’t take hits or act, so it’s not much use, but hey, they can attack… conceptually!
I’d like to point out that you want to break down features and tasks like we’re doing since a few lessons when developing software. Sometimes, like with our combat system, it takes time to put all the pieces together and get a visual result. But by now, we’ve set some necessary foundations and wrote real features already.
Next, we’ll create stats, actions, and make battlers both take damage and act.