Queueing player turns

In this short lesson, we are going to queue the player battler’s turns.

If multiple player-controlled characters become ready to act simultaneously, we want to keep them on standby until the player finished a turn. Once we add menus, we don’t want more than one to pop up on the screen.

We need to keep track of when the player is in menus. However, all our turn code is wrapped in a single function. But we can’t inspect that function’s state and see if a party member is currently acting easily. So we need a new property to tell us that the player is currently playing a turn. If that is the case and another player-controlled battler reaches the end of the timeline, we add it to a stack.

Note that this added complexity is only necessary if time keeps moving forward during a player’s turn. I’ve chosen to keep the game this way in the demo to push players to make decisions quickly.

Open ActiveTurnQueue.gd and let’s get coding.

# Emitted when a player-controlled battler finished playing a turn, that is, when
# the `_play_turn()` function returns.
# We're going to use it to play the next battler's turn.
signal player_turn_finished

# If `true`, the player is currently playing a turn
var _is_player_playing := false
# Stack of player-controlled battlers that have to take turns.
var _queue_player := []


func _ready() -> void:
    # Our new signal allows us to use a function to start the next turn.
    connect("player_turn_finished", self, "_on_player_turn_finished")
    #...

We need to update _on_Battler_ready_to_act(), the callback that receives battlers that reached a _readiness of 100.

func _on_Battler_ready_to_act(battler: Battler) -> void:
    # If the battler is controlled by the player but another player-controlled battler is in the middle of a turn, we add this one to the queue.
    if battler.is_player_controlled() and _is_player_playing:
        _queue_player.append(battler)
    # Otherwise, it's an AI-controlled battler or the player is waiting for a turn, and we can call `_play_turn()`.
    else:
        _play_turn(battler)

The next task is to update the value of _is_player_playing and emit our new signal. We’re going to do so in our existing _play_turn() method.

func _play_turn(battler: Battler) -> void:
    if battler.is_player_controlled():
        # ...
        # set_time_scale(0.05)
        # We assign `true` to the variable at the start of a player-controlled battler's turn.
        _is_player_playing = true

    #...
    # yield(battler, "action_finished")
    # At the very end of the function, if it's a player-controlled battler ending their turn, we emit our new signal.
    if battler.is_player_controlled():
        emit_signal("player_turn_finished")

We’re missing our signal callback, _on_player_turn_finished(), to play turns in the queue.

func _on_player_turn_finished() -> void:
    # When a player-controlled character finishes their turn and the queue is empty, the player is no longer playing. 
    if _queue_player.empty():
        _is_player_playing = false
    # Otherwise, we pop the array's first value and let the corresponding battler play their turn.
    else:
        _play_turn(_queue_player.pop_front())

With this extra code, player-controlled battlers now wait in line to act.

The code so far

Here is what ActiveTurnQueue.gd should contain so far:

# Queues and delegates turns for all battlers.
class_name ActiveTurnQueue
extends Node

signal player_turn_finished

var is_active := true setget set_is_active
var time_scale := 1.0 setget set_time_scale

var _party_members := []
var _opponents := []
var _is_player_playing := false
var _queue_player := []

onready var battlers := get_children()


func _ready() -> void:
    connect("player_turn_finished", self, "_on_player_turn_finished")
    for battler in battlers:
        battler.connect("ready_to_act", self, "_on_Battler_ready_to_act", [battler])
        if battler.is_player_controlled():
            _party_members.append(battler)
        else:
            _opponents.append(battler)


func set_is_active(value: bool) -> void:
    is_active = value
    for battler in battlers:
        battler.is_active = is_active


func set_time_scale(value: float) -> void:
    time_scale = value
    for battler in battlers:
        battler.time_scale = time_scale


func _play_turn(battler: Battler) -> void:
    var action_data: ActionData
    var targets := []

    battler.stats.energy += 1
    battler.is_selected = true

    var potential_targets := []
    var opponents := _opponents if battler.is_party_member else _party_members
    for opponent in opponents:
        if opponent.is_selectable:
            potential_targets.append(opponent)

    if battler.is_player_controlled():
        battler.is_selected = true
        set_time_scale(0.05)
        _is_player_playing = true

        var is_selection_complete := false
        while not is_selection_complete:
            action_data = yield(_player_select_action_async(battler), "completed")
            if action_data.is_targeting_self:
                targets = [battler]
            else:
                targets = yield(
                    _player_select_targets_async(action_data, potential_targets), "completed"
                )
            is_selection_complete = action_data != null && targets != []
        set_time_scale(1.0)
        battler.is_selected = false
    else:
        action_data = battler.actions[0]
        targets = [potential_targets[0]]

    var action = AttackAction.new(action_data, battler, targets)
    battler.act(action)
    yield(battler, "action_finished")

    if battler.is_player_controlled():
        emit_signal("player_turn_finished")


func _player_select_action_async(battler: Battler) -> ActionData:
    return battler.actions[0]


func _player_select_targets_async(_action: ActionData, opponents: Array) -> Array:
    return [opponents[0]]


func _on_Battler_ready_to_act(battler: Battler) -> void:
    if battler.is_player_controlled() and _is_player_playing:
        _queue_player.append(battler)
    else:
        _play_turn(battler)


func _on_player_turn_finished() -> void:
    if _queue_player.empty():
        _is_player_playing = false
    else:
        _play_turn(_queue_player.pop_front())