Creating attack actions

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

Implementing the attack logic

To attack a target, we’re going to use a typical object-oriented approach that consists of two steps:

  1. Creating an object that represents the attack’s impact.
  2. Passing it to the battler to consume.

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:

  1. It allows and forces you to pass a single value to the battler when it should take damage.
  2. It encapsulates only the information the battler needs to know about the attack’s effect.

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.