Problem With Projectile in a Micro Turn-Based RPG

Godot Version

4.3

I have successfully setup most of a micro, turn-based RPG game. There are two attack types, one where the character himself moves towards the opponent and one where he shoots a projectile. The attack and heal moves are defined as a resource, but the main action happens in the character script which is shared by both player and enemy. The problem i am facing is that while the basic attack action is taking place as expected, the same thing happens when a fireball projectile is supposed to be hurled towards the opponent.

The resources, character and projectile scripts are as follows:

Resource

extends Resource
class_name CombatAction
enum Type {
  Attack,
  Heal
}
@export var display_name : String
@export var action_type : Type
@export_category("Damage")
@export var damage : int
@export var projectile_scene : PackedScene
@export_category("Heal")
@export var heal_amount : int
@export var heal_scene : PackedScene

Character

extends Area2D
signal on_health_change
signal on_die(character : Area2D)

@export var combat_actions : Array[CombatAction]
@export var opponent : Area2D
@export var is_player : bool
@export var character : Area2D
@export var current_health : int = 25
@export var max_health : int = 25

var start_position : Vector2

var attack_opponent : bool
var current_combat_action : CombatAction

func _ready():
    start_position = position
    get_parent().connect("on_begin_turn", on_begin_turn)
    
    
func heal(health_amount : int) -> void:
    current_health += health_amount
    
    if current_health > max_health:
      current_health = max_health
    
    emit_signal ("on_health_change")
    
   
    
func cast_combat_action(combat_action : CombatAction) -> void:
    current_combat_action = combat_action
    if combat_action.damage > 0:
        attack_opponent = true
    elif combat_action.projectile_scene != null:
        var projectile_instance : Sprite2D = combat_action.projectile_scene.instantiate()
        projectile_instance.initialize(opponent, get_parent().end_turn)
        get_parent().add_child(projectile_instance)
        projectile_instance.position = position
        attack_opponent = false
    elif combat_action.heal_amount > 0:
        heal(combat_action.heal_amount)
        get_parent().end_turn()


func _process(delta):
  if attack_opponent:
    position = position.move_toward(opponent.position, delta * 2500)
  if attack_opponent == false:
    position = position.move_toward(start_position, delta * 1500)
    
    
  if position.x == opponent.position.x and attack_opponent:
    attack_opponent = false
    opponent.take_damage(current_combat_action.damage)
    
    
    get_parent().end_turn()
    
func take_damage(damage_to_take : int) -> void:
    current_health -= damage_to_take
    emit_signal("on_health_change")
    
    if current_health <= 0:
      pass
    
    
func on_begin_turn(c: Area2D) -> void:
  if character == c:
    determine_combat_action()
    
    
func determine_combat_action() -> void:
    var health_percentage : float = get_health_percentage()
    var want_to_heal : bool
    var ca : CombatAction
    
    if health_percentage <= 35:
      var random : int = randi() % 3
    
      if random == 0:
          want_to_heal = true
    
    if want_to_heal and has_combat_action_of_type(CombatAction.Type.Heal):
      ca = get_combat_action_of_type(CombatAction.Type.Heal)
    elif has_combat_action_of_type(CombatAction.Type.Attack):
      ca = get_combat_action_of_type(CombatAction.Type.Attack)
        
        
      if ca != null:
        cast_combat_action(ca)
        
      else:
       get_parent().end_turn()

    
func get_health_percentage() -> float:
    return float(current_health) / float(max_health) * 100
   
func has_combat_action_of_type(type : CombatAction.Type) -> bool:
  for element in combat_actions:
    if element.action_type == type:
      return true
  return false
    
func get_combat_action_of_type(type : CombatAction.Type) -> CombatAction:
  var available_actions : Array[CombatAction] = []
  
  for element in combat_actions:
    if element.action_type == type:
      available_actions.append(element)
      
  return available_actions[randi() % available_actions.size()]

Projectile

extends Sprite2D
 
signal hit_callback

@export var damage : int
@export var speed: float

var target : Area2D

func initialize(projectile_target : Area2D, on_hit_callback: Callable) -> void:
    target = projectile_target
    hit_callback.connect(on_hit_callback)
    
func _process(delta):
    if target == null:
        return
        
    position = position.move_toward(target.position, speed * delta)
    
    if position == target.position:
        target.take_damage(damage)
        emit_signal("hit_callback")
        queue_free()

Please let me know if you need further information to help me!

I have the feeling the problem lies here:

elif combat_action.projectile_scene != null:
        var projectile_instance : Sprite2D = combat_action.projectile_scene.instantiate()

when you check if the projectile_scene from your combat_action resource returns null, youre not checking if it is currently instanced, youre making that operation on the resource. Im not certain, but I think that youre checking if the resource’s RID has been released yet (which it wont be, because its a resource thats being referenced by your characters node)

a new resource isnt instanced every time you call get_combat_action_of_type because the whole array exists as beloning to the node in your exported combat_actions array. I’m not super familiar with using resources like this, but I think checking the action_type of the enum, rather than the scene, would work better, as it can only ever be one enumeration or another

elif combat_action.action_type == CombatAction.Type.Attack :
        var projectile_instance : Sprite2D = combat_action.projectile_scene.instantiate()
        projectile_instance.initialize(opponent, get_parent().end_turn)
        

No that aint working!

This code works for one move with the projectile:

func cast_combat_action(combat_action : CombatAction) -> void:
    current_combat_action = combat_action
    if combat_action.damage > 0:
        attack_opponent = true
    #elif combat_action.projectile_scene != null:
        var projectile_instance : Sprite2D = combat_action.projectile_scene.instantiate()
        projectile_instance.initialize(opponent, get_parent().end_turn)
        get_parent().add_child(projectile_instance)
        projectile_instance.position = position
        attack_opponent = false
    elif combat_action.heal_amount > 0:
        heal(combat_action.heal_amount)
        get_parent().end_turn()

But then i get the following error

“Cannot call method ‘instantiate’ on a null value.”

“E 0:03:31:0205 swap_chain_resize: Condition “err != VK_SUCCESS” is true. Returning: ERR_CANT_CREATE
<C++ Source> drivers/vulkan/rendering_device_driver_vulkan.cpp:2606 @ swap_chain_resize()”