Coding a customizable JRPG random action system?

Godot Version

4.3 Stable

Question

I’m making an open source turn based JRPG, I want any programmer to be able to very easily create new enemies with different behaviors by simply customizing export variables in the editor, There’s currently 3 Actions the enemies can do: Attack, Strong Attack, Do nothing.

I want it so that each enemy can have a specific chance to do each action, So for example there could be a “Lazy” enemy that has an 80% chance to do nothing and a 20% chance to actually attack, Another enemy could be “Aggressive” with a 75% chance to attack and a 25% to do a strong attack.

But I’m having trouble implementing this system, I’m attempting to use @export_range and have users just increase the chances of each action happening, But that didn’t work so easily:

@export_category("Chance for actions")
@export_range(0.01, 1.00) var attackingChance
@export_range(0.01, 1.00) var strongAttackingChance
if !enemyGettingReadyForStrongAttack:
	var action: float = randf()
	if action <= enemyData.attackingChance:
		enemy_attack()
	elif action <= enemyData.strongAttackingChance:
		enemy_strong_attack_prep()
	else:
		enemy_nothing()
else:
	enemy_strong_attack()

This current system works when I only have 2 actions those being an action that can the user can manipulate the chance of + a default action, So the user can set the attacking chance to 65% and if randf() returns 0.65 or below then the enemy will attack and if it doesn’t then it’ll just default to doing nothing.

But this won’t work when more than 2 actions are active, The if statement prioritizes the 1st if and skips the other elifs if the 1st if just turns out to be true, So If I have the attacking chance be 60% and strong attacking be 40% and randf() returns 0.61 to 1.00 then the enemy will default to doing nothing, But if randf() returns 0.01 to 0.60 then the if statement will just do the normal attack and never do the strong attack since the if statement has a certain order.

I’m really not sure how to code this system ! Please tell me any ideas you have, I’m ready to just rewrite the whole system if necessary.

You can just reverse the order.

if !enemyGettingReadyForStrongAttack:
	var action: float = randf()
	if action <= enemyData.strongAttackingChance:
		enemy_strong_attack_prep
	elif action <= enemyData.attackingChance:
		enemy_attack()
	else:
		enemy_nothing()
else:
	enemy_strong_attack()

The trick is to always check for lowest probability first and then increase the probability with every if statement

1 Like

Thanks for the reply, I understand what you’re saying but this won’t solve my issue, I want it to be fully customizable, I want there to potentially be an enemy that has a 80% chance to do a strong attack and a 20% chance to do a normal attack for example, And in the future I’m gonna add more actions like Defending and Magic, So You’re proposal won’t really solve the issue.

I tried asking ChatGPT and I think it gave me some good code ? I’m too tired to try it now though

@export_range(0.0, 1.0) var attackingChance: float
@export_range(0.0, 1.0) var strongAttackingChance: float
@export_range(0.0, 1.0) var defendingChance: float
@export_range(0.0, 1.0) var magicChance: float

func _ready():
    normalize_action_chances()

func normalize_action_chances():
    var total_chance = attackingChance + strongAttackingChance + defendingChance + magicChance
    
    if total_chance == 0.0:
        push_error("Total chance is 0! Set at least one action chance.")
        return

    # Normalize to make the sum 1.0
    attackingChance /= total_chance
    strongAttackingChance /= total_chance
    defendingChance /= total_chance
    magicChance /= total_chance

func choose_action():
    var action = randf()
    var lower_bound = 0.0

    # Attack action
    var upper_bound = lower_bound + attackingChance
    if action >= lower_bound and action < upper_bound:
        enemy_attack()
        return

    # Strong attack action
    lower_bound = upper_bound
    upper_bound = lower_bound + strongAttackingChance
    if action >= lower_bound and action < upper_bound:
        enemy_strong_attack_prep()
        return

    # Defend action
    lower_bound = upper_bound
    upper_bound = lower_bound + defendingChance
    if action >= lower_bound and action < upper_bound:
        enemy_defend()
        return

    # Magic action
    lower_bound = upper_bound
    upper_bound = lower_bound + magicChance
    if action >= lower_bound and action < upper_bound:
        enemy_cast_magic()
        return

    # Default to doing nothing if no action matches
    enemy_nothing()

in this case you can create an array/dictionary containing the percentages and then sort it and go through the if statement and execute the corresponding method

1 Like

That sounds more clean and elegant, Can you explain this system more ?

There are several ways to do this. One way is to create a custom resource that has two values:

  1. probability
  2. method to call
    First step for every ability, create a resource and save them inside an array and sort it (you can use a custom sort method for this) (if you want to change probabilities mid game you just have to sort it again).
    Then you create a for loop where you go through this sorted array (from lowest probability to highest) and if a case is true you call the method thats associated with the probability.

I can provide a code example if you want

The chance amounts always have to add up to 100% or the math is going to be flawed.
So for most enemies I would assume that “Do Nothing” takes up anything that doesn’t add up to 100% but you could export that value as well.

One way to handle this is with a simple array.
ex. 65 items in a 100 element array give a 65% chance of being picked at random:

#Switch to integers   
@export_range(1, 100) var attackingChance:int
@export_range(1, 100) var strongAttackingChance:int
var actions:Dictionary = { 
   "Attack":enemy_attack, 
   "StrongAttack":enemy_strong_attack_prep, 
   "DoNothing":enemy_nothing
}
...
var doNothingChance:int = 100 - attackingChance + stringAttackingChance
var action:Array[String]
for n in attackingChance: 
   action.append("Attack")   
for n in strongAttackingChance: 
   action.append("StrongAttack")   
for n in doNothingChance: 
   action.append("DoNothing")     

#action.shuffle()       # you can shuffle this array but I don't think this is at all necessary
actions[action.pick_random()].call()
1 Like