Dragon20C
Dragon20C

Reputation: 361

How would I add other types of actions(status effects, heal, stat boosts, party switching) to a turn based system

I am currently trying to implement some different actions instead of attack, but I want to focus on a party function, that looks like this, I can figure out the types of actions I want but I want to change the battle function to fit into a role that can accept multiple actions and not just attack

func change_party(inventory,index): # Inventory being if enemy or player
    if inventory[index].fainted == false: # Checks if the monster is not dead
        var hold = inventory[0] # Holds the first slot and then applys it later to the selected slot
        inventory[0] = inventory[index]
        inventory[index] = hold
    

This changes the index that was inputed by buttons and swaps it around with the first slot in the array this is because the first slot is what monster is shown first, I also have this battle function:

func _battle():
    participants = [player_inventory[0],enemy_inventory[0]]
    participants.sort_custom(self, "check_speed")
    for attacker in participants:
        var player_move = player_inventory[0].move_slot[action_index]
        var random_move = randi() % 3 + 1
        var enemy_move = attacker.move_slot[0] # Use random_move currently 0
        
        var target = _choose_target(attacker, participants)
        if attacker == player_inventory[0]:
            yield(attack(attacker, target, player_move), "completed")
        else:
            yield(attack(attacker, target, enemy_move), "completed")
            
        if player_inventory[0].current_hp <= 0:
            player_inventory[0].current_hp = 0
            player_inventory[0].fainted = true
            Battle_Ui_Updater()
            self.current_state = GameState.LOST
            return
        if enemy_inventory[0].current_hp <= 0:
            enemy_inventory[0].current_hp = 0
            enemy_inventory[0].fainted = true
            Battle_Ui_Updater()
            self.current_state = GameState.WON
            return

    self.current_state = GameState.ACTION

One solution that came to me was trying to have all the actions in a array and just call the action I want based on input but I have no clue how I would make that look readable or bug-free

In this function is how it decides who´s turn it is based on speed, but sometimes I want the player to go first for example when I want to change party members and when the player has changed I want the enemy to start attacking, But I am scratching my head on how would I make it change actions, I know the attack function should be changed if I want it to do something else but I also want to be able to control who´s turn based on what type of action is used, I am sorry if I didnt explain it well, hope you guys understand, I don't want to repeat my self by making another similar to battle function so how do I avoid being repetitive while also doing what I want?

BattleScript:

func attack(attacker, target, move):
    print(str(attacker.name) + " used " + str(move.name) + " " + str((move)))
    var new_text = (attacker.name + " attacked with " + move.name)
    text_scroller(new_text)
    var data = recieve_damage(move,attacker,target)
    #var data = target.take_damage(move,attacker) # Calls the function to take damage
    yield(get_tree().create_timer(2), "timeout") #Wait for 2 seconds
    Battle_Ui_Updater()
    if data:
        yield(critical_hit(attacker),"completed")
        
func critical_hit(attacker):
    var new_text = (attacker.name + " has landed a critical hit! ")
    text_scroller(new_text)
    print(attacker.name + " has landed a critical hit!")
    yield(get_tree().create_timer(2.5), "timeout")

func Get_effectiveness(attack_type,defence_type):
    if attack_type == TypeData.types.none or defence_type == TypeData.types.none:
        return 1
    print("row : " + str(attack_type))
    print("col : " + str(defence_type))
    return TypeData.chart[attack_type][defence_type]

func recieve_damage(action,attacker,defender):
    var critical = 1
    var critical_chance = randi() % 100 + 1
    if critical_chance <= 6.25:
        critical = 2
    var attack_mode
    var defence_mode
    if action.is_special == true:
        attack_mode = attacker.special_attack
        defence_mode = defender.special_defence
    else:
        attack_mode = attacker.attack
        defence_mode = defender.defence
    
    var type : float = Get_effectiveness(action.type,defender.type_1) * Get_effectiveness(action.type,defender.type_2)
    var modifier : float = rand_range(0.85,1.0) * type * critical
    var a : float = (2.0 * attacker.level / 5.0 + 2.0)
    var b : float = (a * attack_mode * action.power / defence_mode) / 50.0
    var c : float = (b + 2.0) * modifier
    var damage = int(c)
    defender.current_hp -= damage
    print(str(attacker.name) + " = " + str(damage))
    return critical > 1

Swap Party member with the current one:

func change_party(inventory,index):
    if inventory[index].fainted == false:
        var hold = inventory[0]
        inventory[0] = inventory[index]
        inventory[index] = hold
        print(inventory[0].hp)
        Battle_Ui_Updater()
        move_ui_updater(player_inventory[0])
        yield(get_tree().create_timer(2),"timeout")

Upvotes: 1

Views: 400

Answers (1)

Theraot
Theraot

Reputation: 40295

I came up with this:

class_name Task

class Skipper:
    signal skip
    
    func emit() -> void:
        emit_signal("skip")

var _instance:Object
var _method:String
var _parameters:Array
var _result = null

func _init(instance:Object, method:String, parameters:Array = []) -> void:
    _instance = instance
    _method = method
    _parameters = parameters

func execute():
    var instance = _instance
    var parameters = _parameters
    if instance != null and instance.has_method(_method):
        _instance = null
        _parameters = []
        _result = instance.callv(_method, parameters)
        if _result is GDScriptFunctionState && _result.is_valid():
            _result = yield(_result, "completed")
            return _result

    var skipper = Skipper.new()
    skipper.call_deferred("emit")
    yield(skipper, "skip")
    return _result

And this is how you initialize it:

var parameters = [player_inventory[0], enemy_inventory[0], player_inventory[0].move_slot[action_index]]

player_action = Task.new(self, "attack", parameters)

And this is how you use it:

yield(player_action.execute(), "completed")

The new thing about this class is that it will be asynchronous regardless of whether the method it calls is asynchronous or not (so you don't have to worry if what it calls yields or not). And it will complete after the method it calls completes, either way. And it even returns (null if what it calls does not return)!

Note: This code will memoize the result, and get rid of the parameters and instance it was linked to. Subsequent calls to execute will simply return the memoized result. This way it does not hold references unecesarily.


How do we do that?

Well, first of all, we are using callv to call a method by name with an array of parameters. There is a class FuncRef in Godot that can be used in similar fashion. However, not using it resulted more convinient.

Second, calling something asynchronous would be yield(..., "completed") but we don't know if what we are calling is asynchronous. So we check. How? If it is asynchronous, it actually returns the state it left the execution as a GDScriptFunctionState, so we check that.

If it is GDScriptFunctionState, then we can use yield(..., "completed") on it.

If it isn't. We need to somehow make the function asynchronous. We are going to do that by emitting a signal with call_deferred. I decided in making it an inner class so that it is not exposed outside of the script.

Also note that the code checks if the instance is not null and has the method passed. But if it does not, it will simply return the result it had stored. This is part of the memoization mechanism, however, it also means that you have no feedback if you passed the wrong instance of wrote the method name wrong.


Finally, it should work with this version of _battle:

func _battle():
    participants = [player_inventory[0],enemy_inventory[0]]
    participants.sort_custom(self, "check_speed")
    for current_participant in participants:
        if current_participant == player_inventory[0]:
            yield(player_action.execute(), "completed")
        else:
            yield(enemy_action.execute(), "completed")

        var state = decide_battle_state()
        if state != GameState.BATTLE:
            self.current_state = state
            return

    self.current_state = GameState.ACTION

func decide_battle_state():
    if check_fainted(player_inventory[0]):
        return GameState.LOST

    if check_fainted(enemy_inventory[0]):
        return GameState.WON

    return GameState.BATTLE

func check_fainted(participant) -> bool:
    if participant.current_hp > 0:
        return false

    participant.current_hp = 0
    participant.fainted = true
    Battle_Ui_Updater()
    return true

You could make an Task with speed:

class_name BattleAction extends Task

# warning-ignore:unused_class_variable
var speed

func _init(instance:Object, method:String, parameters:Array = []).(instance, method, parameters) -> void:
    pass

Then use like this:

var parameters = [player_inventory[0], enemy_inventory[0], player_inventory[0].move_slot[action_index]]

player_action = BattleAction.new(self, "attack", parameters)
player_action.speed = player_inventory[0].speed

And finally _battle can look like this:

With your BattleAction which has speed, you can do this:

func _battle():
    actions = [player_action, enemy_action]
    actions.sort_custom(self, "check_speed")
    for action in actions:
        yield(action.execute(), "completed")
        var state = decide_battle_state()
        if state != GameState.BATTLE:
            self.current_state = state
            return

    self.current_state = GameState.ACTION

Upvotes: 1

Related Questions