How is a complex RPG damage system typically done?

Godot Version

4.3

Question

I’m currently making a Rougelike with complex damage system…So far I haven’t written a single function on damage, since I’m worrying too much on the future need.

Let’s say I have an attacker A, and a defenser B.
attack
The most basic damage calculation would be simple,

B.hp -= A.attack - B.defence

Sums up the whole interaction.

Now.
What if my character has a swappable weapon, and it would increase an attack, and the defender also has a shield?

B.hp -= (A.attack + A.weapon.attack) - (B.defence + B.weapon.defense)

It’s already becoming a mess. What if I have multiple weapons?

var damage: int = 0

damage += A.attack
for weapon in A.weapons:
	damage += weapon.attack

damage -= B.defence
for weapon in B.weapons:
	damage -= weapon.defence

B.hp -= damage

Perfect, now I have no worry stacking up weapons.

Now. What if I want a magic weapon, that has low base stat, but bypass defence check?

...
var bypass_defence: bool = false
for weapon in A.weapons:
	if weapon.is_magic:
		bypass_defence = true
...
if !bypass_defence:
	damage -= B.defense

Cool, but what if B has damage reduction on magic attack specifically, but the basic attack stat from A still deals full damage?
Oh wait, I also want an item that makes you deal damage base on opponent’s attack, an area buff that add 30% to fire damage, a passive effect that heals 10% of the damage you done…

==================================
I think I’ve illustrate my point enough.
I can’t just expand the damage calculation script forever.
I’ll have to make it modular somehow, and that’s where I’m stuck for the past few days.
The closest thing I have for now, is just stacking up effect after effect in an array, and ask for the defender to do calculation.

class_name DamageModifier extends Resource
var raw: int = 0
var multiplier: float = 1.0
var priority: int = 0
var trait: Array[StringName]
var modifiers: Array[DamageModifier]
modifiers.append(A.basic_attack)
modifiers.append(B.basic_defence)
for modifier_stacks in [A.weapons, A.items, B.weapons, B.items]:
	for modifier in modifier_stacks:
		modifiers.append[modifier]
B.take_damage(modifiers)
func take_damage(modifiers: Array[DamageModifiers])
	modifiers.sort_custom(func(a, b): return a.get("priority") <= b.get("priority"))
	for modifier in modifiers:
		## Calculation logic here, probably

And then I look at this, go:
What if I want to heal on attack? The damage goes one way, A wouldn’t know how much it dealt…
What if the weapon does damage base on specific item effect? I’ll have to make it be aware data on other modifiers in the array…

======================================
The questions themselves don’t actually matter,
My point is, I’m always worrying about imaginary problem that doesn’t exist yet.
Once the imaginary problem got solved, I just move the goalpost and worrying more. This also can’t go on forever.

So, I’m making this post to ask, how is this typically done?

This is a solved problem right? How to construct and build an interface for complex damage calculation, is there some kind of demonstration?
Or does everyone actually just build simple system, and build hack upon hack until refactor is necessary, I’m actually fine and should just move on?

I’ve been stuck in my own head for half a week now, any opinion is welcomed.
Thanks for reading.

I’m afraid I can’t give you a silver bullet to solve all of your problems, and I doubt one exists.

If I read between the lines, I get the impression you don’t have a concrete idea yet on what your game’s combat system is going to be. Am I right in this assumption? You’re thinking a lot about things that might potentially happen during combat, and what all possible edge cases are. This is good, but you should take it one step further and make a list of your combat system’s requirements. It’s a lot easier to write code if you know what the final product should look like in advance, what features it has, and what features it does without.

Are you familiar with software patterns? I can’t (and shouldn’t) tell you what to do, but the ‘strategy’ pattern seems useful here. You could make, for instance, a base class called attackStrategyBase with one (empty) method that takes an enemy as a parameter:

class AttackBaseStrategy
{
	public:
		AttackBaseStrategy()=default;
		~AttackBaseStrategy()=default;
		
		virtual void applyToEnemy(Enemy enemy) = 0;
};

And then make a whole bunch of classes that extend that base class with everything you could possibly want:

// Header
class SetOnFireStrategy : public AttackBaseStrategy
{
	public:
		SetOnFireStrategy()=default;
		~SetOnFireStrategy()=default;
		
		virtual void applyToEnemy(Enemy enemy) override;
};

// Source
void SetOnFireStrategy::applyToEnemy(Enemy enemy)
{
	enemy.addEffect(Effects::Burning);
}
// Header
class DoDamageStrategy : public AttackBaseStrategy
{
	public:
		DoDamageStrategy()=default;
		~DoDamageStrategy()=default;
		
		virtual void applyToEnemy(Enemy enemy) override;
};

// Source
void DoDamageStrategy::applyToEnemy(Enemy enemy)
{
	enemy.doDamage(150);
}
// Header
class StealItemStrategy : public AttackBaseStrategy
{
	public:
		StealItemStrategy()=default;
		~StealItemStrategy()=default;
		
		virtual void applyToEnemy(Enemy enemy) override;
};

// Source
void StealItemStrategy::applyToEnemy(Enemy enemy)
{
	if (enemy.hasItem())
	{
		player.addItem(enemy.items.first());
	}
}

You can then mix and match these strategy classes as you please. For example, the three classes above could be used to create a single attack that deals 150 damage to an opponent, sets them on fire, and steals the first item in their inventory if they have any.

5 Likes

To add to TokyoFunkScene’s answer: if you don’t already know how complex your system will be, start simple and adapt it to your needs. There are a lot of different possibilities how to implement such a system, and different developers do it differently.
Although thinking about your architecture beforehand is generally a good thing, it can also hinder you from making progress if you overdo it or don’t have a specific concept of what you want to achieve.

5 Likes

Appreciate the comments.
More or less what I’ve already learnt, but having confirmation from outside saves a lot of self doubt.

Just now, I realized the true issue underlying my thought process:
Not really the over thinking part, but the fact that I instantly scratch an implementation based on what it can’t do.
What I could’ve simply done, is just list out what the implementation CAN’T do, then implement it anyway.
It doesn’t have to be perfect, and I can simply acknowledge the short coming and decide not to act on it for the moment.
If it needs to be tackled, changing a system is easier than building perfect solution from scratch; if it doesn’t, it wasn’t worth the time anyway.

I’m marking the post as solved. The title question wasn’t really answered, but it’s a bad question anyway.
Thanks for everyone’s time, further comments are welcomed though.

3 Likes

Many simple combat systems treat magic weapons as weapons that do more damage, increase the chance to hit, or both. In those systems damage is just damage.

For example, 3 points of fire damage is just 3 points of damage. 3 points of being stabbed with a knife, 3 points of damage.

Someone in an RPG design journal once said types of damage are mostly descriptive aids. Dead is dead, doesn’t really matter how it happened. :rofl:

1 Like

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