Dragon20C
Dragon20C

Reputation: 361

How do I add speed to my turn based system so if a monster has more speed it attacks first

I made a function that compares the speeds of two monsters the player and the enemy but I dont know how to make them do their attack and then go back to a state where the player can press a button again, at the moment the player attacks and then the enemy attacks without speed being a factor if I change the button to check_speed it becomes an endless loop

This is the code:

func check_speed(): # Gonna make it so speed becomes a factor
    if P_team.monster_invetory[0].speed > enemy_scene.speed:
        player_attack()
    else:
        enemy_attack()
        
func player_attack(): # Attacks the enemy
    print("Player attacking")
    enemy_scene.take_damage(P_team.monster_invetory[0].attack) # Calls the function to take damage
    yield(get_tree().create_timer(2), "timeout") #Wait for 2 seconds
    Enemy_battle_scene(enemy_scene) #This updates the display values
    
    var is_dead = enemy_scene.take_damage(P_team.monster_invetory[0].attack) # Checks if dead
    if is_dead:
        current_state = GameState.WON
    elif !is_dead:
        current_state = GameState.ENEMYTURN
        enemy_attack() # Here is where the enemy decides what to do, for now it only attacks
        
func enemy_attack(): # Attacks the player
    print("Enemy attacking")
    P_team.monster_invetory[0].take_damage(enemy_scene.attack) # Calls the function to take damage
    yield(get_tree().create_timer(2), "timeout") #Wait for 2 seconds
    Player_battle_scene(P_team.monster_invetory[0]) #This updates the display values
    
    var is_dead = P_team.monster_invetory[0].take_damage(enemy_scene.attack) # Checks if dead
    if is_dead:
        current_state = GameState.LOST
    elif !is_dead:
        current_state = GameState.PLAYERTURN
        #player_attack()

func _on_Attack_button_pressed():
    if current_state != GameState.PLAYERTURN:
        return
    player_attack()
    #check_speed()

Upvotes: 1

Views: 515

Answers (1)

Theraot
Theraot

Reputation: 40295


New Answer

A state machine by any other name.

You have states:

enum GameState {START, CHOICE, PLAYERTURN, ENEMYTURN, WON, LOST, CAPTURE}
var current_state = GameState.START

We can make that more useful by turning current_state into a property and adding a "state_changed" signal:

signal state_changed()
enum GameState {START, CHOICE, PLAYERTURN, ENEMYTURN,WON, LOST, CAPTURE}
var current_state setget set_current_state
func set_current_state(new_value):
    current_state = new_value
    emit_signal("state_changed")

Now, every time we set self.current_state, it will trigger the signal. And of course we can connect to it.

So we can do this:

func _ready():
    self.connect("state_changed", self, "_on_state_changed")
    self.current_state = GameState.START

func _on_state_changed():
    match current_state:
        GameState.START:
            _start()
        GameState.CHOICE:
            _choice()
        GameState.PLAYERTURN:
            _player_turn()
        GameState.ENEMYTURN:
            _enemy_turn()
        GameState.LOST:
            _lost()
        GameState.CAPTURE:
            _capture()

Why not put _on_state_changed directly in set_current_state? Because the signal is asynchronous. Also, you don't have to handle them all on this script. You may even have a script for each state.

Alright, let us start making those functions:

START:

func _start():
    # initalization stuff
    self.current_state = GameState.CHOICE

CHOICE:

func _choice():
    # We have to wait for the button press, we do nothing here
    pass
    
func _on_Attack_button_pressed():
    if current_state != GameState.CHOICE:
        return

    if _player_speed() > _enemy_speed(): # Checks which monster is faster
        self.current_state = GameState.PLAYERTURN
    else:
        self.current_state = GameState.ENEMYTURN

With some definition of _player_speed and _enemy_speed.

PLAYER_TURN and ENEMY_TURN:

var enemy_attk = false
var player_attk = false

func _player_turn():
    yield(_player_attack(), "completed")
    if _enemy_is_dead():
        self.current_state = GameState.WON
    else:
        if enemy_attk:
            player_attk = false
            enemy_attk = false
            self.current_state = GameState.CHOICE
        else:
            player_attk = true
            self.current_state = GameState.ENEMYTURN

func _enemy_turn():
    yield(_enemy_attack(), "completed")
    if _player_is_dead():
        self.current_state = GameState.LOST
    else:
        if player_attk:
            player_attk = false
            enemy_attk = false
            self.current_state = GameState.CHOICE
        else:
            enemy_attk = true
            self.current_state = GameState.PLAYERTURN

With some definition of _player_is_dead, _enemy_is_dead, _player_attack and _enemy_attack.

WON, LOST and CAPTURE:

func _lost():
    # whatever happens
    pass
    
func _won():
    # whatever happens
    pass
    
func _capture():
    # whatever happens
    pass

I have no idea.


Now, let us merge PLAYERTURN, ENEMYTURN into a new BATTLE state. Update the enum:

enum GameState {START, CHOICE, BATTLE, WON, LOST, CAPTURE}

Update the match statement:

func _on_state_changed():
    match current_state:
        GameState.START:
            _start()
        GameState.CHOICE:
            _choice()
        GameState.BATTLE:
            _battle()
        GameState.LOST:
            _lost()
        GameState.CAPTURE:
            _capture()

When the button is pressed, we simply change to BATTLE:

func _on_Attack_button_pressed():
    if current_state != GameState.CHOICE:
        return

    self.current_state = GameState.BATTLE

And now the main act:

func _battle():
    if _player_speed() > _enemy_speed(): # Checks which monster is faster
        yield(_player_attack(), "completed")
        if _enemy_is_dead():
            self.current_state = GameState.WON
        else:
            yield(_enemy_attack(), "completed")
            if _player_is_dead():
                self.current_state = GameState.LOST
            else:
                self.current_state = GameState.CHOICE
    else:
        yield(_enemy_attack(), "completed")
        if _player_is_dead():
            self.current_state = GameState.LOST
        else:
            yield(_player_attack(), "completed")
            if _enemy_is_dead():
                self.current_state = GameState.WON
            else:
                self.current_state = GameState.CHOICE

Let us talk about working with objects. First of all, have references to them. You already have enemy_scene, let us use it. We need to add a counter part player_scene.

onready var player_scene = P_team.monster_invetory[0]

By the way, if I were you, I'd add a script to EnemyPos and PlayerPos so I can do this:

$EnemyPos.update_view(enemy_scene)
$PlayerPos.update_view(player_scene)

Now, START can look like this:

func _start():
    _update_view()
    print("Battle started")
    yield(get_tree().create_timer(0.2), "timeout")
    self.current_state = GameState.CHOICE

func _update_view():
    $PlayerPos.update(player_scene)
    $EnemyPos.update(enemy_scene)

A suggestion: Add a "health_changed" signal to those scenes, and connect it to $PlayerPos or $EnemyPos respectively, so they can update automatically. In fact, we could use that signal to handle when the monsters die.

The attack is an issue, because these objects do not know their target. Will worry about that later.

Now update BATTLE to work with player_scene and enemy_scene:

func _battle():
    if player_scene.speed > enemy_scene.speed:
        yield(attack(player_scene, enemy_scene), "completed")
        if enemy_scene.current_health == 0:
            self.current_state = GameState.WON
        else:
            yield(attack(enemy_scene, player_scene), "completed")
            if player_scene.current_health == 0:
                self.current_state = GameState.LOST
            else:
                self.current_state = GameState.CHOICE
    else:
        yield(attack(enemy_scene, player_scene), "completed")
        if player_scene.current_health == 0:
            self.current_state = GameState.LOST
        else:
            yield(attack(player_scene, enemy_scene), "completed")
            if enemy_scene.current_health == 0:
                self.current_state = GameState.WON
            else:
                self.current_state = GameState.CHOICE

func attack(attacker, target):
    print(attacker.name, " attacking")
    target.take_damage(attacker.attack) # Calls the function to take damage
    yield(get_tree().create_timer(2), "timeout") #Wait for 2 seconds
    _update_view()

Please note that with a "health_changed" signal this code is much simpler. In that you won't have to handle winning and losing conditions on _battle, nor call _update_view from attack.

Finally, we can write a version that puts them in an array:

func _battle():
    var participants = [player_scene, enemy_scene]
    participants.sort_custom(self, "check_speed")
    for attacker in participants:
        var target = _choose_target(attacker, participants)
        yield(attack(attacker, target), "completed")

        if player_scene.current_health == 0:
            self.current_state = GameState.LOST
            return
        if enemy_scene.current_health == 0:
            self.current_state = GameState.WON
            return

    self.current_state = GameState.CHOICE

func check_speed(a, b):
    return a.speed > b.speed:

func _choose_target(attacker, participants):
    for participant in participants:
        if participant == attacker:
            continue

        return participant

Again, this would be simpler with a "health_changed" signal.

This would still require some modification to fully support more than two monsters:

  • To win it would check if all enemy monsters are dead, not just the one.
  • To lose it would check if all ally monsters are dead, not just the one.
  • When choosing target make sure it picks of the appropriate team considering the attacker. You may also consider given control to the player to choose targets.

You can, of course, keep the array outside of _battle and remove participants when they die. Then checking if all enemy monsters are dead, is actually checking if all the remaining monsters are allies (and viceversa). Which you could do by keeping the count of how many ally or enemy monsters are there, and decrement it as they die. And you can do that connecting to the "health_changed" signal.


Ah, yes, a version that allows a monster to attack multiple times according to its speed. You will need a new attribute on these scenes. I'll call it exhaustion, it should be 0 at the start of the battle.

func _battle():
    var participants = [player_scene, enemy_scene]
    var player_attacked = false
    while (true):
        var attacker = get_next(participants)

        if player_scene == attacker:
            if player_attacked:
                # It is time for the player to attack again.
                # Return control to the player.
                self.current_state = GameState.CHOICE
                return
            else:
                player_attacked = true

        var target = _choose_target(attacker, participants)

        yield(attack(attacker, target), "completed")
        attacker.exhaustion += 1.0 / attacker.speed # <--

        if player_scene.current_health == 0:
            self.current_state = GameState.LOST
            return
        if enemy_scene.current_health == 0:
            self.current_state = GameState.WON
            return

func get_next(participants):
    participants.sort_custom(self, "check_order")
    return participants[0]

func check_order(a, b):
    if a.exhaustion < b.exhaustion:
        return true

    if a.exhaustion == b.exhaustion:
        return a.speed > b.speed:

    return false

As you can see here, the rule is that the monster with less exhaustion goes first. If two monsters have the same exhaustion, the one with more speed goes first. And every time a monster attacks, its exhaustion is incremented. By how much? The inverse of speed. Thus monsters with less speed get exhausted quicker, and monsters with more speed get exhausted slower. As a consequence, if a monster has more speed than the other, it will end up doing multiple attacks for each one that the first one does.

Note: I called it "exhaustion" just so you have some intuition of how the value changes. However, it is important that this is not kept from battle to battle, it must be reset to 0. If you imagine an exhausted monster vs a not exhausted one, the not exhausted one will get a lot of attacks in before the other one can do anything.

We need to keep track if the player attacked, as I mentioned in the original answer, to break the loop.


Original answer

It depends on how exactly you want Speed to work, I'll only show the simplest approach. But first let us start at a base line.

Let us say, that disregarding speed, the player attacks, then the monster attacks, and then it waits for next input.

That means doing this something like this:

func _on_Attack_button_pressed():
    # ...
    player_attack()
    enemy_attack()

Except those function yield. So we are going to need this:

func _on_Attack_button_pressed():
    # ...
    yield(player_attack(), "completed")
    yield(enemy_attack(), "completed")

The reason is that a function which yields returns a GDScriptFunctionState.

Documentation on says GDScriptFunctionState:

Calling @GDScript.yield within a function will cause that function to yield and return its current state as an object of this type

Here I'm using the "completed" signal of GDScriptFunctionState as suggested in the documentation on Coroutines & signals:

If you're unsure whether a function may yield or not, or whether it may yield multiple times, you can yield to the completed signal conditionally


Now, the simplest way to have "speed" work is to treat speed as turn order. Similar to D&D initiative. That means that all we do is sort those operations according to speed. So you do something like this:

func _on_Attack_button_pressed():
    # ...
    if player_goes_first():
        yield(player_attack(), "completed")
        yield(enemy_attack(), "completed")
    else:
        yield(enemy_attack(), "completed")
        yield(player_attack(), "completed")

Giving your code, I'm guessing player_goes_first will simply return P_team.monster_invetory[0].speed > enemy_scene.speed. See Functions.

We could also write it like this:

func _on_Attack_button_pressed():
    # ...
    if player_speed() > enemy_speed():
        yield(player_attack(), "completed")
        yield(enemy_attack(), "completed")
    else:
        yield(enemy_attack(), "completed")
        yield(player_attack(), "completed")

Where player_speed and enemy_speed are functions that return the appropriate values.

If you needed three participants, you can imagine the code (something like this).


However, your code would benefit from using objects with a speed property and an attack method (Those objects may or may not be nodes, see Classes).

If you have objects, it would be it easier to support more participants:

  • Put the participants in an array.
  • Sort them by speed, with sort_custom.
  • Iterate over the array, and call attack on each one.

Another approach is to have a get_next function that returns the next object on which to call attack. Then call get_next on a loop. You would have a variable to keep track of whether or not the player has attacked (by checking if the what you got from get_next is the player object), and stop before the loop player attacks twice (since the user is supposed to press the button to attack again).

Using a get_next function would allow for more complex logic. For example, have a participant attack multiple times according to its speed.

Upvotes: 1

Related Questions