Hiya again, I’m back with the next steps for making the Turn System! (Well, I suppose the actual turn system itself is already completed with the functioning state machine, but I want to fully flesh out the selecting and performing of each of the unit’s turns)
Today the focus is going to be on the Overlord and monster phases. The code for the
MonsterActionPhase will look almost identical to the code for the
HeroActionPhase, since monsters are people too.
…Okay, maybe not. But the code will still remain very similar. So similar in fact that I wonder whether I should combine the
HeroActionPhase and the
MonsterActionPhase into one base class. That would probably be the wise thing to do, but since when have I been wise? I’m going to not worry about that particular good-practice right now and just continue on, since I’m eager to keep up my momentum.
I’ll start by copying all of the contents of the
hero_action_phase.gd script and pasting it into the
monster_action_phase.gd script. Then I’ll rename a few variables and change the
class_name value back to
MonsterActionPhase so that the top of the script now looks like this.
extends State class_name MonsterActionPhase var monster: Unit var action_points: int var leftover_movement: int func _init(sm: StateMachine, unit: Unit).(sm, "MonsterActionPhase"): monster = unit action_points = 2 leftover_movement = 0
Now, the monsters don’t get as many action choices as the heroes do. Specifically, they aren’t able to do the Stand Up action or the Revive action, since when a monster is defeated they’re removed from the game. They also aren’t able to do the Rest action since they don’t have stamina points.
The other major difference between a hero’s turn and a monster’s turn is that monsters are only allowed to spend one action point on an attack during their turn, whereas heroes are allowed to use both action points on attacking, if they so choose.
So with all of that in mind, let’s modify the
_get_available_actions() function to remove the unavailable action types, as well as make an additional check when determining whether to allow the Attack action.
var has_attacked: bool func _init(sm: StateMachine, unit: Unit).(sm, "MonsterActionPhase"): # ... has_attacked = false # ... func _get_available_actions(): var available = [UnitActions.Actions.end_turn] if action_points <= 0: if leftover_movement > 0 and UnitActions.can_do_move_action(monster): available.append(UnitActions.Actions.move_extra) else: if not has_attacked and UnitActions.can_do_attack_action(monster, null): available.append(UnitActions.Actions.attack) if UnitActions.can_do_move_action(monster): available.append(UnitActions.Actions.move) if UnitActions.can_do_interact_action(monster): available.append(UnitActions.Actions.interact) if UnitActions.can_do_skill_action(monster): available.append(UnitActions.Actions.skill)
As a side note, I moved the
Actions enum to the
UnitActions singleton since both the heroes and monsters will be using it.
Then all we need to do is modify the
_action_selected() function to remove the actions that cannot be selected! We can also set the
has_attacked variable in the
func _action_selected(action): # ... match action: # ... UnitActions.Actions.Attack: has_attacked = yield(UnitActions.do_attack_action(monster, "heroes"), "completed")
And that pretty much covers the modifications we need to make to the
In the last post we made a
GUIManager with a
HeroTurnGUI scene. Because the monster group phase will behave very similarly to the hero group phase I think we can repurpose that GUI to serve both the heroes and monsters! The only differences will be which actions are available, which we already covered in the last section.
Firstly, since this will now be used by both the heroes and the monsters I think it makes sense to rename it to something less hero-centric. I’ve opted to rename it to
UnitTurnGUI, and I’ll accordingly change the function name in
GUIManager to be
get_unit_turn_gui() instead of
get_hero_turn_gui(), as well as remove the “gui_type” metadata and instead just assign the scene to a group “unit_gui”.
The other change I thought to make for the
UnitTurnGUI is to change the column count of the button row to match the number of visible buttons. That way we won’t get overflowing buttons like we saw in the demo last week.
The function now looks like this:
func enable_buttons(action_list: Array, hide_disabled_buttons = false): var count = 0 for action in _btn_map.keys(): var btn = _btn_map[action] if action_list.has(action): btn.disabled = false btn.visible = true else: btn.disabled = true btn.visible = ( false if ( hide_disabled_buttons or action == UnitActions.Actions.stand or action == UnitActions.Actions.move_extra ) else true ) if btn.visible: count += 1 _button_grid.columns = count
Unit activation order
Another key thing we’ll need to be able to do is select which units we want to activate when. Currently we’re just activating the units in the order they’re in within the scene tree, but part of the strategy of the game will be determining the order of operations for your units’ activations.
I think that selecting the unit’s avatar image might work, so let’s try that out.
Let’s create a new script and name it something like
avatar_selection_gui.gd. This GUI will hold the list of
CharacterAvatar scenes previously held by the
UnitTurnGUI, so we can remove the reference to that from
UnitTurnGUI and add it to the
AvatarSelectionGUI. There are two ways to go about doing this, one would be to sub-class the
UnitTurnGUI to the
AvatarSelectionGUI. The other would be to create a new scene with the
AvatarSelectionGUI as the scene root node. I like the second approach more since it keeps the UI responsibilities separate, so that’s what I’ll do.
We’re going to add three new functions to the
AvatarSelectionGUI, one to enable selection, one to disable selection, and one to set which avatars should be shown in grayscale (indicating they’ve already been activated).
Let’s start with the
func enable_avatar_selection(disabled_options = ): for i in range(_avatar_list.avatars.size()): if disabled_options.has(_unit_list[i]): # If the avatar is disabled we don't want # to connect the following callbacks. continue var avatar = _avatar_list.avatars[i] # The "clicked" signal is a custom signal on character_avatar.gd # The mouse_entered and mouse_exited are signals provided by the Control node avatar.connect("clicked", self, "_on_clicked_avatar", [avatar]) avatar.connect("mouse_entered", self, "_on_hover_over_avatar", [avatar, true]) avatar.connect("mouse_exited", self, "_on_hover_over_avatar", [avatar, false]) func disable_avatar_selection(): for avatar in _avatar_list.avatars: avatar.disconnect("clicked", self, "_on_clicked_avatar") avatar.disconnect("mouse_entered", self, "_on_hover_over_avatar") avatar.disconnect("mouse_exited", self, "_on_hover_over_avatar")
_on_clicked_avatar() function will emit a custom defined signal,
avatar_clicked, along with the unit the avatar represents. How do we know which unit the avatar represents? I’m glad you asked! In the
set_avatar_list() function that we brought over from the
UnitTurnGUI we’re given a list of units. From those units we instantiate all the avatar scenes we need. All we need to do is add an extra line of code in that function to link the avatar with the unit it represents.
Then in the
_on_clicked_avatar() function, we just need to put the following.
The other function about hover enter and exit will set the border outline to white or black, depending on whether it’s entering or exiting.
func _on_hover_over_avatar(avatar, entering): if entering: avatar.border_color = Color.white else: avatar.border_color = Color.black
As for the grayscaling, I just added another option to the
CharacterAvatar shader indicating whether it should be grayscaled or not, and the
grayscale_avatars() function will set this option on the desired avatars.
And that pretty much covers it!
Testing it out
The last thing to do is replace the current
_select_next_monster() functions with using this functionality instead of using the next unit in the list. This is as simple as connecting to the
avatar_clicked function on the
AvatarSelectionGUI and starting the selected unit’s turn.
# For simplicity I'm using the term "unit" rather than "hero" # or "monster" but the function will behave the same for both. func _select_next_unit(): var gui = GUIManager.get_avatar_selection_gui() gui.set_avatar_list(units) gui.set_header_text("Select unit...") gui.grayscale_avatars(_have_finished_turn) gui.connect("avatar_clicked", self, "_start_unit_turn", , CONNECT_ONESHOT) gui.enable_avatar_selection(_have_finished_turn) func _start_unit_turn(unit): GUIManager.get_avatar_selection_gui().disable_avatar_selection() # Change state to unit turn start phase ...
We’re now able to select which unit we want to activate, and you can see the columns get resized when there’s an extra action available. The attack action is also only available once per turn for monsters. Sweet! We’ve verified our code works!
Now, if all we had to do were select between any monster on the screen then our work would be done! (Minus one or twelve bug fixes) However, the Overlord’s monster phase works by selecting a monster group, and then activating all monsters within that group before moving on to the next group. That means that we need yet another layer of selection logic.
Fortunately, because we separated the
AvatarSelectionGUI from the
UnitTurnGUI we only need to worry about the first in the
OverlordMonsterPhase. However, we’ll need to tweak the
AvatarSelectionGUI slightly in order to accommodate non-unit nodes (since the
OverlordMonsterPhase will be concerned with nodes that have units as their children, rather than the units themselves).
Fortunately this won’t be too much of a hassle. We’ll just add a second parameter option to the
is_unit_group. This will affect the function in the following way.
func set_avatar_list(units, is_unit_group = false): _unit_list = units var avatars =  for unit in units: if not unit or (is_unit_group and unit.get_children().size() == 0): continue var avatar = _avatar_scene.instance() var meta_key if is_unit_group: # We're assuming that all children of the unit group node # are of type "Unit" avatar.character_sprite = unit.get_children().unit_data.sprite meta_key = "linked_units" else: avatar.character_sprite = unit.unit_data.sprite meta_key = "linked_unit" avatar.set_meta(meta_key, unit) avatars.append(avatar) _avatar_list.set_avatar_list(avatars, true)
Then when in the callbacks like the
_on_hover_over_avatar() we just need to check for which meta key is defined before using the appropriate one.
if avatar.has_meta("linked_units"): ...
And that’s it! Obviously I didn’t provide every new line of code I wrote, but I hope that I explained my process well enough for people to follow along and fill in the spaces themselves. Here’s a quick demo of checking that the unit selection process is working seamlessly with the state machine.
The code for all the posts up to this point can be found here.