Turn Based Combat Tutorial - English
Turn Based Combat Tutorial - English
In this tutorial I assume the user already has a little experience using Godot and I will skip some
basic steps, still it doesn’t pretend to be a guide for very experienced devs, and I hope everyone will
be able to follow it easily
The final project of this tutorial will consist in a simple battle scene with 6 characters (the player
will select the 3 ones they wanna use and they will fight against the other 3). The characters will
have different stats (health points, attack, defense, and speed), one or two types that will affect how
the different attacks affect it, and 3 attacks that may have different effects. They will fight in a turn
based combat; the player will choose the moves of their characters and the enemies will act
following an attack guide.
This project will not have an open world, an experience and level system, objects, the ability to
choose the character’s atttacks… but I’m confident it will help the person trying to do a game like
that to learn a method to program the combat part of their game and being able to implement those
ideas later.
For this tutorial I will use Godot 4.3 and assets from Kenney (https://kenney.nl/), although I think
the tutorial can be followed easily in sightly older versions of the program and the user can use
whatever sprites and ideas they want to.
INDEX
O create our characters we gonna do custom resources. A resource is a way to store data; Godot has
its own resources, like texures, datafont… but it also let us create our own.
To create a custom resource, in fileSystem we create a new script inheriting resource.
In this file I want to define all the characteristics of each character “race”. Even if in this tutorial it
will only be a character of each race, it may be useful in a game, if for example, we want to fight a
group of cyclops; we use he cyclops base for all of them, and they characteristics may vary because
of its level, for example.
For tthis tutorial I’m gonna show the code first and then explaining it; this is our race script.
extends Resource
class_name race_stats
We have to give our resource type a class_name to be able to assign it later to the resources.
Then we have the vasiables I made for each race; we need at least one sprite (if there’s different
sprites if the characer is an ally or an enemy, we can add here both),a decription the player will see
when selecting their party characters, 2 types (if a character only have one type, the second one will
be a blank string, but we need both variables), their attack, defense, health points and speed stats,
and an array will all the moves it can have. In this tutorial it will be a string array, each string will
be the attack name, but in your case you may need more information, like the level it will learn that
move.
You can see I made the attack and defense stats floats; that’s because we’re gonna divide them, and
for the division to get an accurate result we need them o be floats.
Another variables you can define here are for example how much experience that race will give you
or how many experience it will need to level up.
All those variables need an @export so we can modify them in each race. We can guve them a base
value to edit laer or leave tthem blank, but we need to assign them a type (string, int, float, bool,
array, dictionary, vector…)
Once we have our resource base done we’ll create a new resource; right click in the fileSystem,
create new > resource, it will open a list will all the recource types Gotod have, we search here for
our resource classname and select it. We create as many resources as races we’re gonna have and
name them. If you click them you can edit them on the inspector. I’ll save all these files on a folder
to make everything tidy.
Here you can see my races resources and how my cyclops resource variables are defined but the
attacks; we write those later.
For the sprites I used this asset pack https://kenney.nl/assets/tiny-dungeon .
Also I saved a tinny hand to use as cursor form here https://kenney.nl/assets/cursor-pixel-pack
Now t that our resources are done, I create a new scene; itt will be the base scene of all our
characters. We create a Node2D as base node and add a script to it. As nodes of the sceren we create
a sprite2D, a progressBar, and an animationPlayer.
func _ready():
assing_stats()
func assing_stats():
$Sprite2D.texture = race_stats.sprite
attack = race_stats.attack * 0.1 * level
defense = race_stats.defense * 0.1 * level
max_health = race_stats.max_health * 0.1 * level
base_speed = race_stats.speed * 0.1 * level
current_health = max_health
$ProgressBar.max_value = max_health
$ProgressBar.value = current_health
First we create our export variable in witch we’re gonna call our resource. We define our character
stats, and we define them in _ready() (I defined them in a separate function that I call in ready)
using the race stats and the character level. I could not define the stats and just use the race ones,
simce I’m not gonna have diferent stats, but this way you can see how to do it.
I applied a linear grown of the stats as the level increases; you may need a different function
depending on what do you need.
As you see I separate the speed in 2 variables; some of my attacks will hit with priority and I’ll use
the speed to check it. You may also need some variables if you’re gonna change your base stats (or
justt reset them calling attack = race_stats.attack * 0.1 * level again).
We also apply the image texture to the sprite and give the progress bar the values it needs.
We should be careful to nott edit he variables inside the race_stats resource, since that will edit the
resource itself and alter all the nodes depending on it.
I’ll also assign a texture to the sprite in the inspector; if we run the game this sprite will be replaced
to the correct one, but his way we’ll see how the sprites will look in the viewer
We edit the scale in our sprite (in my case 4x4) and edit the progress bar as we please; I toggle off
the percentage view and added 2 stylebboxflatts in control > theme overrides > styles with the color
I liked and some borders. You can editt the value to see how itt looks both empty and full.
Also we create some animations with the animationPlayer; we’ll edit our sprite2D.
A damage animation will flick in red and move sightly up and down, and a heal animation that will
blow in green. Also, a death animation in witch the sprite will shrink and become black. All of them
will be 0.5 seconds long.
Once the character scene is almost done, we’ll do our battle scene.
To make the background and dialog boxes I’m gonna use sprite2D with GradientTexture1Ds as
texture (you can use drawings or photos to get a better look; I’m trying to make the tutorial with
free easily accessible assets on in-engine resources only). I made a label settings resource to make
the font bigger and add a border, if you have a specific font you want to use you can use it here too.
I saved it so I can reuse it in every text I’ll use. Don’t forget to activate autowrap mode in your label
and align the borders with your dialog box.
This is how my scene is looking.
3) Creating the main scene script and coding the selecting character section:
The first thing we are gonna code in the main scene is the selection menu.
extends Node2D
var non_selected_characters : Array
var allies_array : Array
var enemies_array : Array
var battle_state
enum STATES {PREBATTLE}
var selected
func _ready():
battle_state = STATES.PREBATTLE
for child in $characters.get_children():
non_selected_characters.append(child)
selected = non_selected_characters[0]
$selection_hand.position = selected.position
func _input(event):
match battle_state:
STATES.PREBATTLE:
selecting_allies(event)
func selecting_allies(event):
if event.is_action_pressed("ui_right") or event.is_action_pressed("ui_down"):
selected = non_selected_characters[non_selected_characters.find(selected) + 1]
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
selected = non_selected_characters[non_selected_characters.find(selected) - 1]
$selection_hand.position = selected.position
First the variable arrays; we need an array to cycle around our characters; non selected characters is
gonna have all the characters so we can select them. We could put the characters there manually, but
you can see in ready() that we search the children inside our characters node and append it
automatically. The allies and enemies array are not in use still but we’ll need them later.
An array is just a list with a bunch of elements inside in a specified order. You’ll need the characters
to be sorted inside the characters node in the same order you place them in the game space visually.
After we put the characters in the array we assign the first position of the array to the selected
variable. We’ll talk about this later, but you can observe that to select an element of an array you
write the array name and then the position of the element inside square brakes. The first position has
the number 0, the second one has the number 1 and so on.
Then we have battle state and the enum STATES; we’re gonna make a simple state machine to
command correctly the battle options we’ll had.
Right now states only have PREBATTLE, this will describe the state in witch we select the
characters who integrate our party, another states will determine if we are selecting with character is
gonna attack, witch attack it’s gonna use, or witch enemy it’s gonna hit.
The variable selected is gonna be the character we have selected, and will dictate the arrow position
and other information.
The function _ready is triggered once when the scene is opened; well put in there all the things we
need to define right at the beginning of the game.
We select out battle state, we fill the non selected characters with all the characters, and we select
the first character in that array and get the arrow to select it. (if your arrow node has another name
you have to name it correctly in the code).
The input function gets triggered every time you make an input (a keyboard key, a mouse click or
movement…) and it’s the function we are gonna use for almost the entire game. We could put
almost all our code in there, but I prefer to make separate functions for everything and just call them
from here to make everything tidy.
We’ll use the match to see in witch state of the battle we are, and depending of the state, we are
triggering one function or another. Right now we only have the selecting allies function defined.
The event in brackets is the input, so we need to past it to the new function.
If you write the code in selecting allies directly inside of input the program will help you to auto-
complete it. You may need to write it in input and then paste it in you function if you’re not sure
what you’ll need.
In selecting allies first the function checks if were pressing the right and down arrows with
event.is_action_pressed("name of the input"), if it detects that we are, then it runs the code line
selected = non_selected_characters[non_selected_characters.find(selected) + 1].
What does this line do? non_selected_characters.find(selected) will give us the number of the
position the selected character occupies in the array. If we just print it well get 0, 1, or whatever
position is selected at the moment. So, non_selected_characters.find(selected) + 1. is actually just
an addition of 2 numbers; the position plus one. Here we’re doing the same thing as in ready with
selected = non_selected_characters[0] but changing 0 with another number.
Right after the function checks if the input is the other keys around to run the next line of code, that
instead of add one position to the array, it subtract it. And after that, we move the arrow to the new
selected position.
I’m using the default inputs Godot has, but you can get custom ones in project > project settings >
input map, so you can put whatever in event.is_action_pressed("name of the input").
If we run the game now we can see that the arrow will move around the characters. If we go
backwards it runs perfectly, but if we go forward it will crash wen we try to go forward the last
character; this is because an array don’t have negative posittions, so it asumes tthat before 0 there’s
the last position, but any positive number is viable, so at searching for 5+1, the game will crash
searching for the 6 position on an array that does not have it. To fix that we’re editing the code.
if event.is_action_pressed("ui_right") or event.is_action_pressed("ui_down"):
if non_selected_characters.find(selected) == non_selected_characters.size()-1:
selected = non_selected_characters[0]
else:
selected = non_selected_characters[non_selected_characters.find(selected) + 1]
Here we are checking if the input is the keys we selected to go forward, if the position if the same as
the size of the array -1. The -1 is because the array positions start in 0, so the 5ft position actually
will have the number 4. If this is correct it means we are in the last character of the array, so we’ll
need to make the new selected the first position.
If not, the function will run as before.
One more thing we wanna add to this menu is the name, description and types of the character in
the bottom dialog box, so we’ll add this bloc of code after the arrow position:
We are editing the text on the label that’s on the bottom dialog box (if you used another names for
your nodes write it correctly in your code!). To put something in a label text you need it to be a
string. We used different variables here, like the name of the selected character, or the text that we
wrote in the description or types variables. This works because all those variables are strings, but if
they’re not, or you have doubts if they are, you can stringfy them with str(variable). The strings that
we write directly on code have to be between “” to work. \n inside a string will gives us a line
break.
The if part of the function is because not all characters have a second type; if they don’t, the type_2
variable is blank and we’ll don’t need to add anything more. We check if it’s anything different than
blank, and if it is then we add that little more of text to the label.
If you run now the game you’ll see we cycle trough all the characters showing their description and
types.
Before getting to being able to select the characters, we’re gonna do a confirm selection menu. For
that I’ll add a CONFIRMSELECTION in the enum STATES and I’ll add two “buttons” with yes/no
as dialogue boxes with labels that will be toggled visible off by default
Before the functions we have to create a new variable named button_selected; we’ll use it the same
as selected but for selecting the buttons. This is how the code is looking.
func _input(event):
match battle_state:
STATES.PREBATTLE:
selecting_allies(event)
STATES.CONFIRMSELECTION:
confirm_selected(event)
func selecting_allies(event):
if event.is_action_pressed("ui_right") or event.is_action_pressed("ui_down"):
if non_selected_characters.find(selected) == non_selected_characters.size()-1:
selected = non_selected_characters[0]
else:
selected = non_selected_characters[non_selected_characters.find(selected) + 1]
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
selected = non_selected_characters[non_selected_characters.find(selected) - 1]
$selection_hand.position = selected.position
$dialog/bottomBoxLabel.text = selected.name + ": " + selected.description + "\nType: " +
selected.race_stats.type_1
if selected.race_stats.type_2 ! = "":
$dialog/bottomBoxLabel.text += " / " + selected.race_stats.type_2
if event.is_action_pressed("ui_accept"):
prepare_confirm_selection()
func prepare_confirm_selection():
$confirmButtons.show()
$dialog/topBox.hide()
$dialog/topBoxLabel.hide()
button_selected = $confirmButtons/YesBox
$dialog/bottomBoxLabel.text = "Do you wanna " + selected.name + " in your party?"
battle_state = STATES.CONFIRMSELECTION
func confirm_selected(event):
if event.is_action_pressed("ui_right") or event.is_action_pressed("ui_down") or
event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
if button_selected == $confirmButtons/YesBox:
button_selected = $confirmButtons/NoBox
else:
button_selected = $confirmButtons/YesBox
$selection_hand.position = button_selected.position + Vector2(-15,0)
if event.is_action_pressed("ui_accept"):
if button_selected == $confirmButtons/YesBox:
allies_array.append(selected)
non_selected_characters.erase(selected)
selected.hide()
selected = non_selected_characters[0]
$confirmButtons.hide()
if allies_array.size() == 3:
pass
else:
$dialog/topBox.show()
$dialog/topBoxLabel.show()
battle_state = STATES.PREBATTLE
In prepare confirm selection we show the buttons, hide the top text change the bottom text, give the
yes button node value to our new variable button_selected (we use a new variable and not our
selected variable because we want to be able to access the character that is selected) and change the
state to confirm selection.
In confirm selected we check for all the change selection inputs indifferently since we only have 2
options and changing from one will get us to the other. Once we press the button we check the
current button selected; if its yes, then we change it to no, and if its anything else (that can only be
no) we change it to yes.
Then we move the arrow to the correct selected button; we ad a Vector2 so the arrow will not be on
top of the button. In my case it works right 15 pixels to the left.
Then, if the confirmation button is “yes” we append the character to the allies array, delete if from
the non selected characters, hide the character and then select a new character.
Both if we selected yes or no we hide the buttons, and then we see if we have 3 characters already
selected in the allies array we have a pass that we are gonna edit now. If there’s nor enough
characters on the allies array, we go back to the selection menu.
Now to start the battle we need the positions the characters are gonna have while battling. I’ll make
a diccionary variable at the start of the script with the positions I want to use for the characters.
A dictionary is similar to an array, but the elements in it have the structure of a key (usually a string
but it also can be another value) and the content of the key (that, the same as in an array, can be
anything; a number, a bool, a string, a node as we already did, a vector as we are seeing now, or
even another array or dictionary) The major difference is that while in an array we need the position
to locate an element, a dictionary doesn’t have positions and we need the key.
To get the positions I dragged a character to the desired position and checked in the inspector the
position, then I made the Vector 2 with those values.
Well call the start battle function in the if allies_array.size() == 3: and add CHOOSINGALLY in
our states enum.
func start_battle():
for x in non_selected_characters:
enemies_array.append(x)
for x in allies_array:
x.position = chara_positions["ally"+str(allies_array.find(x))]
x.show()
for x in enemies_array:
x.position = chara_positions["enemy"+str(enemies_array.find(x))]
x.get_node("Sprite2D").flip_h = true
for child in $characters.get_children():
child.get_node("ProgressBar").show()
selected = allies_array[0]
battle_state = STATES.CHOOSINGALLY
func choosing_ally(event):
if event.is_action_pressed("ui_right") or event.is_action_pressed("ui_down"):
if allies_array.find(selected) == allies_array.size()-1:
selected = allies_array[0]
else:
selected = allies_array[allies_array.find(selected) + 1]
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
selected = allies_array[allies_array.find(selected) - 1]
$selection_hand.position = selected.position
$dialog/bottomBoxLabel.text = "Select to choose what will " + selected.name + " do.\
nType: " + selected.race_stats.type_1
if selected.race_stats.type_2 != "":
$dialog/bottomBoxLabel.text += " / " + selected.race_stats.type_2
$dialog/bottomBoxLabel.text += "\nAttack: " + str(selected.attack) + "|= Defense: " +
str(selected.defense) + "|= Speed: " + str(selected.base_speed) + "|= Health points: " +
str(selected.current_health) + " / " + str(selected.max_health)
In start battle we put the remaining characters in the enemies array and position all the characters
correctly.
To get the values inside a diccionary we have to call it like this: dictionary_name[key]. If the key is
a string we have to use the “”. As you see in the code we can combine the string the same way we
do editing a label.text property. Using the position of our character in its array we can get the
correct key in the dictionary.
We also need to turn visible again our allies and to flip the enemies sprites to make them face the
correct direction (if your sprites are looking at the left, or you position your allies at the right you’ll
need to flip your allies instead). Also, I toggle the visivilily of the progress bar off in the scene so
it’s not visible when we open the game; I made itt visible now.
Last, we get our new selected character, who is our first ally and we set the new state.
Since the next step is to program the choosing of the moves the characters are going to do, we’ll
need to have attacks first.
I’ll use a JSON file for this; a json file is like a dictionary but saved outside the script. We can put
all our moves there and all the information we need about them and then call them.
We could also use custom resources, but this way I’ll have all the movements in the same file.
We go to our file system and create a new text file, choose json as its extension. If we click it it’ll
open in the editor as a blank script. We’ll open it with the {} that determine a dictionary. If you
press enter it will tabulate and we’ll construct our json/dictionary vertically, since it will be a big
chunk of code it will be easier like that.
The keys of the elements of this dictionary are the names of the attacks, and the elements will be
also dictionaries, whose keys are the different characteristics of the attacks.
{
"bite":{
"type": "monster",
"description": "A bite using the natural strength of their maw.",
"damage": 50,
"healing": 0,
"is fast": false,
"is slow": false,
"is massive": false
},
"sword slash":{
"type": "warrior",
"description": "Swims around their heavy sword, damaging all the enemies.",
"damage": 30,
"healing": 0,
"is fast": false,
"is slow": true,
"is massive": true
},
"healing spell":{
"type": "mage",
"description": "The user can choose to heal themself or any of their allies.",
"damage": 0,
"healing": 60,
"is fast": true,
"is slow": false,
"is massive": false
},
"energy draining":{
"type": "monster",
"description": "Absorbs their enemy energy, recovering a bit of health points.",
"damage": 30,
"healing": 30,
"is fast":false,
"is slow": false,
"is massive": false
}
}
These are some attacks using all the characteristics I’m gonna add in this tutorial. All attacks need
to have the same elements in their dictionaries. If you want to try different things with your attacks,
you have to put in here elements to check later in code. For example, I didn’t add here a “target
ally” because to check if an attack will target an ally I will check if it does not take damage, but
depends on your attack variety you may need a key like that too. All attacks need a type, a damage
they’ll deal and a description, a healing property that’s 0 in all the attacks that don’t heal, a bool
indicating if it will give it’s user priority, making him attack faster in that turn, or the contrary. The
is massive let us know if the attack will affect all the enemies.
Now I’ll add as many moves as I want my characters to have here, and I’ll add them to the moves
array in each race (for that, go to the inspector, change the array size, in the pencil button of each
value select String, and then you can white the attack name). Be careful to write all the moves
correctly or it will crash our game (same thing writing the types). You can copy-paste them to be
sure. They are capital sensitive.
To add the attacks to our characters we do it in several ways, depending on how the attack learning
works on our game. What I’m gonna do is give 3 random attacks from the race moveset to each
character, so each match the characters have different attacks.
For that, we add this code to the assign stats function, in he character script.
for num in 3:
var new_move = race_stats.moveset.pick_random()
while moves_array.has(new_move):
new_move = race_stats.moveset.pick_random()
moves_array.append(new_move)
For num in number runs the code that number of times. First we creae a variable and assign it one
of the race moves. With while we check it that move is already in the character moves array, itf it
does, we resett the variabel witth a new attack. We have to be careful using a while function, since it
is easy to crash he game entering an infinite loop. For example, here, if a race has less than 3
movements.
Another way to do this is with race_stats.moveset.shuffle() and then use the forst 3 attacks in that
array, but I don’t want to edit anything inside the resource.
Now we’ll do another JSON file where we store the types interactions. We could do that in this
same JSON file since the keys will not match, but I preffer to have it separately. Specially if you
have a lot of attacks and types, it will be quicker to check if they’re different files.
{
"mage":{
"mage": 0.75,
"monster": 1,
"warrior": 1.5,
"": 1
},
"monster":{
"mage": 1.5,
"monster": 0.75,
"warrior": 1,
"": 1
},
"warrior":{
"mage": 1,
"monster": 1.5,
"warrior": 0.75,
"": 1
}
}
This is how our type interactions file looks like. The first key that include a new dictionary inside
are the attack type, and inside of them we’ll check the target type and multiply the damage by the
value. We need the “” key too because well check booth character types, if it doesn’t have a second
type (“” type) ell multiplying it by 1 to not alter the damage.
I decided to make that the same type is not very effective against itself, normal against the type that
kits it hard, and super effective against the type it has advantage against. I also decided to multiply
by 1.5 and 0.75 instead of 2 and 0.5 that are some common values because I don’t want the type to
affect that much the damage, but you can do as you want, even doing different super effectives and
non very effectives values.
As you see adding new types y easy. You can add as amny as you want. Also, it is possible to make
the attack types and the characters types different, as long as you descrive them correcly in this
archive.
Now that we have our attacks defined we’ll get back to out battle scene.
We’ll do a new control node with the attack buttons the same way we do the yes/no buttons. I’ll
order them from bottom to top so if not all moves are filled we don’t have a gap between the moves
and the dialog box. This may be useful when not all characters have the same number of moves. We
also need the names to include the number of the array position. I added 4 buttons even if I’m
finally not using all of them.
We’ll do all the nodes invisible inside the parent node, that will remain visible.
We’ll also add the json files to our script to access them; I create these variables.
Load() will load any file in your file system (use the correct path depending on how and were you
save your files; you can get it by dragging the file form the filesystem to the script). You need to add
.data to convert the json into a dictionary the script can read.
Now we’ll add the CHOOSINGATTACK state to our states enum and add the if
event.is_action_pressed ("ui_accept"): show_attacks() to our choosing ally function, then, create
these new functions and add the select attack in our _input function linked to the choosing attack
state.
func show_attacks():
for pos in selected.moves_array.size():
get_node("attackButtons/"+str(pos)+"Box").show()
get_node("attackButtons/"+str(pos)+"Label").text = selected.moves_array[pos]
get_node("attackButtons/"+str(pos)+"Label").show()
button_selected = selected.moves_array[selected.moves_array.size()-1]
battle_state = STATES.CHOOSINGATTACK
func select_attack(event):
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
if selected.moves_array.find(button_selected) == selected.moves_array.size()-1:
button_selected = selected.moves_array[0]
else:
button_selected = selected.moves_array[selected.moves_array.find(button_selected)+1]
if event.is_action_pressed("ui_right") or event.is_action_pressed("ui_down"):
button_selected = selected.moves_array[selected.moves_array.find(button_selected)-1]
$selection_hand.position = get_node("attackButtons/" +
str(selected.moves_array.find(button_selected)) + "Box").position + Vector2(-65,0)
$dialog/bottomBoxLabel.text = attacks_json[button_selected]["description"] + "\nType: " +
attacks_json[button_selected]["type"] + "\n"
if attacks_json[button_selected]["damage"] != 0:
$dialog/bottomBoxLabel.text += "Power attack: " +
str(attacks_json[button_selected]["damage"]) + ". "
if attacks_json[button_selected]["healing"] != 0:
$dialog/bottomBoxLabel.text += "Healing power: " + str(attacks_json[button_selected]
["healing"]) + ". "
if event.is_action_pressed("ui_accept"):
for child in $attackButtons.get_children():
child.hide()
checking_attack() #crearemos esta función luego, de momento te dará error
get_node(“path”) and $path are interchangeable (even, if possible, $ will be prefered) in a lot of
contexts, but if you need to add part of the path as a variable as I’m doing here, you’ll need
get_node().
A for var in array.size() will go var = 0, 1, 2, etc until the end of the array. We’re using that to get
the attack button and make it visible taking the position number into the name we choose, and give
it the name of the attack with selected.attacks[pos]. As I said before, you need to stringify Any
variable that’s not a string, as for example a number, with str(), to use it like this.
Since the attacks are from bottom to top and we want the one at the top selected we have to search
for it as selected.attacks[selected.attacks.size()-1]
In the event function we change the movement buttons (left for right and up for down) from the
previous code since now the array is inverted.
In $dialog/bottomBoxLabel.text = attacks_json[button_selected]["description"] + "\nType: " +
attacks_json[button_selected]["type"] we are calling our json file for the first time. You can see that
it works exactly like a dictionary. We use it to write different info about the attack in the dialog box.
If you have doubts on what are you calling (or if you’re getting errors), I recommend to add a
print(variable) to see what kind of value you receive to know how o use it.
Now we’ll create a few more variables; a turn_dictionary specifying its a dictionary were we’ll
store our movements on the turn, and a target_selected to select a target without loosing our selected
attacker, and add 3 more states to our state machine; SELECTINGENEMYTARGET,
SELECTINGALLYTARGET and FIGHTING.
func checking_attack():
if attacks_json[button_selected]["is massive"] == true:
turn_dictionary[selected.name] = [selected, button_selected]
check_if_all_attacked()
else:
if attacks_json[button_selected]["damage"] ! = 0:
target_selected = enemies_array[0]
battle_state = STATES.SELECTINGENEMYTARGET
else:
target_selected = selected
battle_state = STATES.SELECTINGALLYTARGET
func selecting_target_enemy(event):
if event.is_action_pressed("ui_right") or event.is_action_pressed("ui_down"):
if enemies_array.find(target_selected) == enemies_array.size()-1:
target_selected = enemies_array[0]
else:
target_selected = enemies_array[enemies_array.find(target_selected) + 1]
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
target_selected = enemies_array[enemies_array.find(target_selected) - 1]
$selection_hand.position = target_selected.position
$dialog/bottomBoxLabel.text = "Do you want to attack " + target_selected.name + " with " +
selected.name + "'s " + button_selected + "?"
if event.is_action_pressed("ui_accept"):
turn_dictionary[selected.name] = [selected, button_selected, target_selected]
check_if_all_attacked()
func selecting_target_ally(event):
if event.is_action_pressed("ui_right") or event.is_action_pressed("ui_down"):
if allies_array.find(target_selected) == allies_array.size()-1:
target_selected = allies_array[0]
else:
target_selected = allies_array[allies_array.find(target_selected) + 1]
if event.is_action_pressed("ui_left") or event.is_action_pressed("ui_up"):
target_selected = allies_array[allies_array.find(target_selected) - 1]
$selection_hand.position = target_selected.position
$dialog/bottomBoxLabel.text = "Do you want to heal " + target_selected.name + "?"
if event.is_action_pressed("ui_accept"):
turn_dictionary[selected.name] = [selected, button_selected, target_selected]
check_if_all_attacked()
func check_if_all_attacked():
if turn_dictionary.size() == allies_array.size():
battle_state = STATES.FIGHTING
assign_enemies_attacks()
else:
battle_state = STATES.CHOOSINGALLY
func assign_enemies_attacks():
for chara in enemies_array:
var attack = chara.moves_array.pick_random()
if attacks_json[attack]["damage"] == 0:
turn_dictionary[chara.name] = [chara, attack, enemies_array.pick_random()]
elif attacks_json[attack]["is massive"] == true:
turn_dictionary[chara.name] = [chara, attack]
else:
turn_dictionary[chara.name] = [chara, attack, allies_array.pick_random()]
The function checking_attack checks if the attack is massive, so it doesn’t need a target, and store it
in our new dictionary.
If the attack is not massive it checks if its an attack that hits an enemy if the damage is different
from zero, or if not (in tthat case it heals an ally), to select the correct state and selected node.
Then you can see the functions we do for that; they are very similar; they select the target and
change the dialog, and store the new turn in our turn dictionary.
Once the attack is correctly stored we check if there’s any more turns to assign, if it is, we go back
to our select ally function, and if not, we change the state to the fighting state (that does nothing for
now) and select random attacks to our enemies.
Right now this function just checks to make a correct array in our dictionary, but when we’ll be
finishing our game we’ll come here again to make a better enemy AI. You can add a
print(turn_dictionary) at the end of the assign enemies attacks to see if the dictionary is correctly
filled with all the characters attacks.
Right now if you select an ally who already did its turn it lets you select it again, and replaces the
old one; when you call a dicionary like dictionary[key] = variable (for example an array in this
case) it will create that key if it does not exist and give it that variable, but if the key already exist, it
will replace its value with the new one. This behavior is desirable here, but we’ll need to edit our
text. We’ll change the $dialog/bottomBoxLabel.text = "Select to choose what will " + selected.name
+ " do.\nType: " + selected.type_1 for this:
if turn_dictionary.has(selected.name):
$dialog/bottomBoxLabel.text = selected.name + " will attack "
if turn_dictionary[selected.name].size() >= 3:
$dialog/bottomBoxLabel.text += turn_dictionary[selected.name][2].name + " "
$dialog/bottomBoxLabel.text += "with " + turn_dictionary[selected.name][1] + ". Select to change turn."
else:
$dialog/bottomBoxLabel.text = "Select to choose what will " + selected.name + " do."
$dialog/bottomBoxLabel.text += "\nType: " + selected.race_stats.type_1
This way we check if the character is already in the dictionary; if not we have the same text as
before, but if it is, we write the attack and, in case its a single target attack, also the target name.
You can also edit this code to make a different text if the character is healing an ally (right now, it
will say that the healed ally is being attacked) for example, checking if the fdamage is ==0 or
checking if the target is in the allies array.
Now you can do a new confirm selection showing the attacks. You can edit the one you have to
know in witch part of the game you are (for example, checking id the dictionary is empty) or make
a new one altogether.
Once you’re happy with how your attack selection works, we’ll start with making the attacks work.
6) Creating the attack sequence:
First we’ll getting the correct turn order, for that we need to transform our turn dictionary into an
array, since you can give an order to an array but not to a dictionary. Create the new array variable
and make this function; we call it right after assigning attacks to our enemies.
func ordering_turn():
for chara in turn_dictionary:
if attacks_json[turn_dictionary[chara][1]]["is fast"] == true:
get_node("characters/"+chara).speed = get_node("characters/"+chara).base_speed+100
elif attacks_json[turn_dictionary[chara][1]]["is slow"] == true:
get_node("characters/"+chara).speed = get_node("characters/"+chara).base_speed-100
else:
get_node("characters/"+chara).speed = get_node("characters/"+chara).base_speed
for turn in turn_dictionary:
turn_array.append(turn_dictionary[turn])
turn_array.sort_custom(sorting_by_speed)
battle_state = STATES.FIGHTING
attack_combo() #definiremos esta función ahora
func sorting_by_speed(a, b):
if a[0].speed >= b[0].speed: return true
else: return false
First we use our dictionary to search if our speed will be altered in this turn for each character. I
added 100 to the speed because my fastest character’s speed is 100, depending on how you manage
your characters speed you may need a different value. You may even add her a variable and edit its
value latter when you know your fastest character speed.
Then we append each array inside our dictionary in the new array; this way we have the same
information in the array and in the dictionary, but without the keys. Then, we rearrange the array.
Arrays have the sort function that will arrange the array in numerical order, or we can use a sort
custom function, in witch we have to define it.
To create a sort custom function you have to pass it 2 variables that will be the elements of the array
to compare, and then define what make it sort one before the other and return true, and return false
for the contrary. In this case, the variables passed will be [attacker, attack, target (in case there’s
any)], so with a[0].speed we are looking for the speed variable in our attacker.
We could also not add the speed variable (only the base speed) and check the bools is fast and its
slow of our characters, if they're the same then check ttheir speeds.
In the fighting state I’m emitting a new signal I create (signal clicked) if I press ui accept, and I
create a new function I’m running after changing the state.
func attack_combo():
for attack in turn_array:
if attacks_json[attack[1]]["is massive"] == true:
$dialog/bottomBoxLabel.text = attack[0].name + " used " + attack[1] + "."
await clicked
if allies_array.has(attack[0]):
for enemy in enemies_array:
$dialog/bottomBoxLabel.text = enemy.name + " looses x health points."
await clicked
else:
for ally in allies_array:
$dialog/bottomBoxLabel.text = ally.name + " looses x health points."
await clicked
elif attacks_json[attack[1]]["damage"] == 0:
$dialog/bottomBoxLabel.text = attack[0].name + " cures " + attack[2].name + "
ussing " + attack[1]
await clicked
else:
$dialog/bottomBoxLabel.text = attack[0].name + " hits " + attack[2].name + "
with " + attack[1] + ". " + attack[2].name + " looses x health points. "
if attacks_json[attack[1]]["healing"] > 0:
$dialog/bottomBoxLabel.text += attack[0].name+" recovered x healt points."
await clicked
turn_array.clear()
turn_dictionary.clear()
selected = allies_array[0]
$selection_hand.show()
battle_state = STATES.CHOOSINGALLY
In this function we still have no functionality to edit character’s health and we’ll edit it later, but we
already have the entire structure of our combat.
We use await signal to stop the function in the desired text until we press accept.
We go trough all the attacks, first we check if it’s massive; if it is we check if the character using it
is an ally or an enemy to target the allies or the enemies and we write the text accordingly.
If the attack is not massive, we check if it doesn’t cause damage (because it is a healing attack) or it
does, and if it does, we also check if it recovers it’s user health. This is because that’s how my
attacks work; I have massive attacks, health attacks, and single target attacks, some of these also
recover the user’s health. If you have other kind of attacks you should write your stricture
accordingly.
Once the fucntion checks all the attacks, it clears the dictionary and array and go back to the
choosing ally state.
You can run it and check everything is working right. Once the turn texts are all displayed it should
let you choose your next turn attacks.
Before coding the damage in the battle scene we’re gonna add this function to he player scene.
func change_health(change):
current_health -= change
if change > 0:
$AnimationPlayer.play("hurt")
elif change < 0:
if current_health > max_health:
current_health = max_health
$AnimationPlayer.play("recover")
var tween = get_tree().create_tween()
tween.tween_property($ProgressBar, "value", current_health, 0.5)
await tween.finished
In this function we subtract the damage the character is gonna receive to their current health; when
the character gets healed we’re gonna use the same function but passing the points healed in
negative. The function checks if that value is positive or negative to play the correct animation, and
applies the new health to the progress bar with a tween that lasts the same as the animation.
If the new health is greater than the max health we make the current health the max one. This one is
not necessary since we’ll also check this later, but unless you want it to be possible to have greater
health than the max health, it’s not bad to check it twice.
Back in the battle scene we’re creating a few more functions and edit the attack combo function;
let’s go little by little. Here we have the functions to add or subtract healthpoints.
func causing_damage(attack, target):
var type_modifier = types_table[attacks_json[attack[1]]["type"]][target.race_stats.type_1]
* types_table[attacks_json[attack[1]]["type"]][target.race_stats.type_2]
var damage = attack[0].attack / target.defense * attacks_json[attack[1]]["damage"] *
type_modifier #if we don’t do the next line of code we need to int() this one
damage = int(damage * randf_range(0.9,1.1)) #optional
target.change_health(damage)
$dialog/bottomBoxLabel.text = target.name + " looses " + str(damage) + " health points.\n"
if type_modifier > 1.2:
$dialog/bottomBoxLabel.text += "It looks like that hurted!"
elif type_modifier < 1:
$dialog/bottomBoxLabel.text += "It wasn't much of a scratch."
if target.current_health <= 0:
await clicked
$dialog/bottomBoxLabel.text = target.name + " can't fight anymore!"
target.get_node("AnimationPlayer").play("death")
await target.get_node("AnimationPlayer").animation_finished
target.hide()
await clicked
func recovering_life(attack, target):
if target.current_health == target.max_health:
$dialog/bottomBoxLabel.text = target.name + " is already at their best!"
else:
var health_recovered = attacks_json[attack[1]]["healing"] * attack[0].max_health/100
health_recovered = int(health_recovered
* randf_range(0.9,1.1)) #optional
if (target.max_health - target.current_health) < health_recovered:
health_recovered = target.max_health - target.current_health
target.change_health(-health_recovered)
$dialog/bottomBoxLabel.text = target.name + " recovered " + str(health_recovered) +
" health points."
await clicked
In both functions we pass the attack array [attacker, attack, target (in case there’s any)], the reason
we also past the target is because in our massive attacks we don have target in our array and try to
acces attack[2] will give us an error.
Type modifier get the value we’ll multiply our damage to get the type modifier;
attacks_json[attack[1]]["type"] will give us the type of the attack, and target.type_1 and
target.type_2 will give us our target types. Then we put there 2 values in our type modifier json to
get the correct modifier; the structure is types_table[attack type][target type].
Then to get the damage we multiply the character attack for the attack damage, and we divide this
to the target defense. We multiply the result for the type modifier.
I add here an optional variation tto the damage, so the same attack doesn’t cause always the same
exact damage. If you don’t want to do this, don’t forget to change youre damage to an int so you
don’t get decimal values.
Once we have the damage we pass it to our target change health function. We need the await to stop
the function until he awaits inside the function finish; if not, everything will happen at the same
time.
Depending on the type modifier I also want to pass a message saying it it’s supper effective or not.
The reason I use 1.2 or 1 it’s because my modifiers are 0.75 and 1.5; if you multiply them you get
1.125, even if this value is a little bigger that 1, I don’t want to consider it supper effective so I put a
number a little bigger that it. You could use some values that don’t get you these numbers (0.5 and 2
work fine) or edit it as it seems right by you.
Also we check if the character dies to play it’s death animation and making it invisible. I don’t
delete it jet because if you delete an element of an array while you're iterating over it you may get
some errors; to prevent this I just make them invisible and check for them at the end of the turn.
In the recovering health function first we check if the character is already at max health to send a
text saying that, if not, we calculate how much health it will gain and check if it’s more that the
health it need to get to it’s max health, if it is, change the health recovered to what it need to get
max health. Since we already edit our change health function to reset the health if it get greater than
max health it’s not technically necessary, but I don’t want the text to say that it recovers more health
that it does.
func attack_combo():
for attack in turn_array:
if attack[0].visible == true:
if attacks_json[attack[1]]["is massive"] == true:
$dialog/bottomBoxLabel.text = attack[0].name +" used "+ attack[1] + "."
await clicked
if allies_array.has(attack[0]):
for enemy in enemies_array:
if enemy.visible == true:
await causing_damage(attack, enemy)
else:
for ally in allies_array:
if ally.visible == true:
await causing_damage(attack, ally)
elif attacks_json[attack[1]]["damage"] == 0:
if attack[2].visible == true:
$dialog/bottomBoxLabel.text = attack[0].name+" used "+attack[1]
await clicked
await recovering_life(attack, attack[2])
else:
$dialog/bottomBoxLabel.text = attack[0].name + "'s try to heal
" + attack[2].name + " is in vain.=="
await clicked
else:
if attack[2].visible == true:
$dialog/bottomBoxLabel.text = attack[0].name+" used "+attack[1]
await clicked
await causing_damage(attack, attack[2])
if attacks_json[attack[1]]["healing"] > 0:
await recovering_life(attack, attack[0])
else:
$dialog/bottomBoxLabel.text = attack[2].name + " is already out
of combat; " + attack[0].name + "'s attack missed."
await clicked
check_if_defeated()
First we check if the attacker is visible (is still alive) and if the targets are also visible in all the
interactions. We change the texts that we had before for the function of causing damage or healing,
depending on witch one we need.
Once all the attacks on the function are done, we run the check if defeated function.
func check_if_defeated():
for child in $characters.get_children():
if child.visible == false:
if allies_array.has(child):
allies_array.erase(child)
child.queue_free()
else:
enemies_array.erase(child)
child.queue_free()
if allies_array.size() == 0 or enemies_array.size() == 0:
if allies_array.size() == 0:
$dialog/bottomBoxLabel.text = "Oh no! You loose"
else:
$dialog/bottomBoxLabel.text = "Congratulations! You win!"
await clicked
get_tree().reload_current_scene()
else:
turn_array.clear()
turn_dictionary.clear()
selected = allies_array[0]
$selection_hand.show()
battle_state = STATES.CHOOSINGALLY
First we check for defeated character (the non visible ones) and delete them and erase them from
their array. Once we deleted all the defeated characters we check if any of the 2 arrays is empty; if it
is, we give a game over text and restart the game. It it not, we run the same code we had before in
our attack combo function; we clear the turn array and dictionary and go back to choose our turn.
With this we have our battle finished, and we could leave it as it is.
But right now the enemies attack at random; in the next part I’m creating a more complex AI for
them to attack.
func assign_enemies_attacks():
for chara in enemies_array:
assign_enemies_attack_2(chara)
ordering_turn()
func assign_enemies_attack_2(chara):
if allies_array.size() >= 3:
for attack in chara.moves_array:
if attacks_json[attack]["is massive"]== true and randf() >= 0.3:
turn_dictionary[chara.name] = [chara, attack]
return
for checkhealth in enemies_array:
if checkhealth.current_health < checkhealth.max_health/4:
for atk in chara.moves_array:
if attacks_json[atk]["damage"]==0 and attacks_json[atk]["healing"]>0 and
randf() >= 0.3:
turn_dictionary[chara.name] = [chara, atk, checkhealth]
return
if chara.current_health < chara.max_health/2:
for atk in chara.moves_array:
if attacks_json[atk]["damage"] > 0 and attacks_json[atk]["healing"] > 0:
for ally in allies_array:
if types_table[attacks_json[atk]["type"]]
[ally.race_stats.type_1]*types_table[attacks_json[atk]["type"]][ally.race_stats.type_2] >= 1.1:
turn_dictionary[chara.name] = [chara, atk, ally]
return
chara.moves_array.sort_custom(sorting_by_attack)
for atk in chara.moves_array:
for ally in allies_array:
if types_table[attacks_json[atk]["type"]]
[ally.race_stats.type_1]*types_table[attacks_json[atk]["type"]][ally.race_stats.type_2] >= 1.5:
turn_dictionary[chara.name] = [chara, atk, ally]
return
if types_table[attacks_json[atk]["type"]]
[ally.race_stats.type_1]*types_table[attacks_json[atk]["type"]][ally.race_stats.type_2]>=1 and randf()>=0.4:
turn_dictionary[chara.name] = [chara, atk, ally]
return
if types_table[attacks_json[atk]["type"]]
[ally.race_stats.type_1]*types_table[attacks_json[atk]["type"]][ally.race_stats.type_2]>0 and randf()>=0.7:
turn_dictionary[chara.name] = [chara, atk, ally]
return
for atk in chara.moves_array:
for ally in allies_array:
if types_table[attacks_json[atk]["type"]]
[ally.race_stats.type_1]*types_table[attacks_json[atk]["type"]][ally.race_stats.type_2] >= 0:
turn_dictionary[chara.name] = [chara, atk, ally]
return
func sorting_by_attack(a, b):
if attacks_json[a]["damage"] >= attacks_json[b]["damage"]: return true
else: return false
Return will stop the function; that why we need 2 functions; the first one search for each enemy,
and the second one search for a good attack to do and then strop running and go back to the first
function.
In he second one, first it checks if we have all our allies, and checks if he enemy has a massive
attack. We also give it a little chance to not run adding randf() >= 0.3 to the conditions; randf gives
us a random number between 0.0 and 1.0.
If this attack does not execute (because we do no have all our allies, because the enemies doesn’tt
have a massive attack, or because he randomness say so) tthen it checks if an anemi is less than ¼
of its life, and checks if it has a healing spell.
If this does not execute neither itt checks if the enemy has less than half of its life and if it has a
attack + recover spell and if it deals supper effective against an ally.
If this does not run then we sort the attacks by power and we check if they’re supper effective
against the allies. We reduce the probably of this attack executing if it’s less efecttive with the hopes
of finding a less powerful attack that does hit supper effective. We don’t need to check if damage’s
> 0 here but if you give immunity to any of your types you should check it.
In case it does not select any attack checking this we go back to the most powerful attack and assign
it to the first ally; I think it’s pretty rare that it does not find any attack by then but just in case so the
enemy does not end with no attack.
This way is possible to assing a target to a massive attack, I don’t think it’s important since we do
not check for massive attacks to not have a target and the attack will work fine.
This function will be heavily influenced in what kind of attacks you have in your game and how do
you want your enemies to behave.
This is the end of this tutorial; I hope it did help you understand how to create a turn based combat
scene and that you could implement it in your projects successfully.