Status Ailment Implementation

Godot Version

4.1.1.stable

Question

I’m making a turn-based RPG game and am trying to implement status ailments. What would be the most elegant way of doing so?

The way I’m doing it now is as follows:

First…
I have a blanket (abstract) class StatusEffect. It holds all the parameters and methods that I may need.

Parameters include IDs, readable name, effect potency, how many turns the effect lasts, etc.

Methods include a function for applying the effect, removing the effect, and empty functions that allow the effect to trigger after certain game events (on_turn_start, on_damage_taken, on_player_death), and so on.

Second…
In order to create a new status ailment, I create a new script.gd that extends StatusEffect and give it a class name NAMEHEREStatusEffect.

From there, I use a _ready() function to change the parameters like id and readable name.

I also redefine the event functions to give the status ailment functionality in game.

Lastly…
Each player has an Array[StatusEffect]. If I want to apply an ailment to a player, I call the apply() method and it gets added to the player.

Anytime an event triggers, it iterates through the array calling the appropriate functions (Ex. a new turn would call the on_new_turn function).

My overall question….
Is this the best way to do so? My main interest and worry is keeping everything neat and organized. Is it fine that each new StatusEffect is in its own .gd file?

Script files are for defining behavior, not properties. For properties use Resources.

You should have:

StatusEffect.tscn
StatusEffect.gd

Whether StatusEffect scene has UI or not is irrelevant, imho, Godot is all about nodes so make the scene inherit from Node instead of Node2D if it has no ui. But each effect would have at least an icon, right? Anyways, I tried to avoid scenes when I started, but it always bit me in the end, hence the recommendation.

If all status effects have same behavior that can be encapsulated in a single Status Effect script file, then just add Configuration property to that script file of type StatusEffectConfiguration, which is just a class extending Resource that contains list of parameters.

Now let’s say player has StatusEffects node (not node2d) to group effects. To test 4 effects, just drop 4 StatusEffect scenes under that, and for each drag-drop a different StatusEffectConfiguration file from your StatusEffects resources folder.

ps: if you do use pure objects and not scenes, make sure their inherit from RefCounted and not Object.

I see. If I’m understanding correctly, I should use Nodes to store status effects and use Resource values to store their individual data.

That would work, but I think I didn’t properly convey what I’m trying to accomplish. So I have some pseudo-code that’ll hopefully clarify why I’m doing this way.

StatusEffect.gd

Burn.gd

Rupture.gd

So to clarify, StatusEffect is a super-class and each individual is a sub-class. The reason I can’t use Resource files is because they all have their own individual functions, they aren’t exclusively parameters.

While I could add ALL status effect functionality in the super-class folder and use the effect resource’s ID as a reference, that’ll look super ugly and cluttered in the long run.

My intention with making all of the effects their own unique class is so I can add them all into an array an iterate over them every time a relevant game even happens.

For example…

var status_effects:Array[StatusEffect] = [BurnStatusEffect.new(), RuptureStatusEffect.new()];

In this example, they player has the “Burn” and “Rupture” status effect. The count doesn’t matter, because if it were 0, it wouldn’t be there.

When the turn ends, we iterate over the array like so…

for se in status_effects: se.on_turn_end();

Since all status effects inherit StatusEffect, and StatusEffect.on_turn_end() is a StatusEffect function, there’ll be no errors.

The main difference is that Burn is the only effect that actually has a response to this event trigger, since we changed it in the Burn.gd script.

This is my intention currently.
My problem was that it felt like there was a better way to do this, because making a script for each seperate status effect felt weird. I didn’t want to dig myself into a whole by using some unorthodox method because I hadn’t put much research into alternative method.

However, I can’t just shove the entire Status Effect system into ONE script because then it’ll end up being 1000+ lines of code, making further expansion of the system very tedious in the future.

Understood.

Ultimately, there’s no right way of doing things. If you’ll only have a dozen status effects, just go with a file for each like you’re doing, it’s clean and easy to extend.

If somehow you’ll have a hundred+ say due to randomly generated combinations, I’d go with a resource system. Or keep it all in code, just call them behaviors. Each behavior has type and damage. Status effect would then hold an array of applicable behaviors. For example…

# StatusEffectBehavior.gd
extends Resource
class_name StatusEffectBehavior

enum Type { OnTurnStart, OnTurnEnd, OnDmgTaken, OnDmgDealt }

@export var potency: float
@export var type: Type

func _init(_type: Type, _potency: float):
	type = _type
	potency = _potency

#StatusEffect.gd
extends RefCounted
class_name StatusEffect
 
var pawn:Pawn2D; #the target
 
var id:String;
var display_name:String;
 
var behaviors: Array[StatusEffectBehavior] = []

# the id and display name and other properties can later be moved into a resource class if more and more properties keep appearing. Something like StatusEffectData. Until then, no need to overcomplicate.
func _init(_id: String, _display_name: String, _behaviors: Array[StatusEffectBehavior]):
	id = _id
	display_name = _display_name
	behaviors = _behaviors

func apply(_behaviors):
	behaviors.append_array(_behaviors)
 
func remove():
	pass;
	#removes self from pawn statuseffect array
 
#EVENT FUNCTIONS. (there's more, this is just a simplified list)
 
func on_turn_start():
	execute_behaviors(StatusEffectBehavior.Type.OnTurnStart):

func execute_behaviors(type):
	var executed = []

	for b in behaviors:
		if b.type == type:
			pawn.take_damage(b.potency);
			executed.append(b)

	for b in executed:
		behaviors.erase(b)

	if behaviors.size() == 0:
		remove() # removes status effect
 
func on_turn_end(): 
	execute_behaviors(StatusEffectBehavior.Type.OnTurnEnd):

func on_damage_taken(): 
	execute_behaviors(StatusEffectBehavior.Type.OnDmgTaken):

func on_damage_dealt(): 
	execute_behaviors(StatusEffectBehavior.Type.OnDmgDealt):

To create them, can do:

var dmg_on_turn_end = StatusEffectBehavior.new(5, StatusEffectBehavior.Type.OnTurnEnd)
var burn_effect = StatusEffect.new("burn", "Burn", [dmg_on_turn_end])

var dmg_on_dmg_taken = StatusEffectBehavior.new(3, StatusEffectBehavior.Type.OnDmgTaken)
var rupture_effect = StatusEffect.new("rupture", "Rupture", [dmg_on_dmg_taken])

var status_effects := [burn_effect, rupture_effect]
2 Likes

That actually works. Other than the events, most of the behavior functions pretty similarly, so this is perfect.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.